diff --git a/.eslintignore b/.eslintignore index 8f5587fe63..8b320a4701 100644 --- a/.eslintignore +++ b/.eslintignore @@ -97,6 +97,9 @@ packages/app-cli/tests/Synchronizer.resources.js.map packages/app-cli/tests/Synchronizer.revisions.d.ts packages/app-cli/tests/Synchronizer.revisions.js packages/app-cli/tests/Synchronizer.revisions.js.map +packages/app-cli/tests/Synchronizer.sharing.d.ts +packages/app-cli/tests/Synchronizer.sharing.js +packages/app-cli/tests/Synchronizer.sharing.js.map packages/app-cli/tests/Synchronizer.tags.d.ts packages/app-cli/tests/Synchronizer.tags.js packages/app-cli/tests/Synchronizer.tags.js.map @@ -784,15 +787,18 @@ packages/lib/BaseApplication.js.map packages/lib/BaseModel.d.ts packages/lib/BaseModel.js packages/lib/BaseModel.js.map +packages/lib/BaseSyncTarget.d.ts +packages/lib/BaseSyncTarget.js +packages/lib/BaseSyncTarget.js.map packages/lib/InMemoryCache.d.ts packages/lib/InMemoryCache.js packages/lib/InMemoryCache.js.map +packages/lib/JoplinDatabase.d.ts +packages/lib/JoplinDatabase.js +packages/lib/JoplinDatabase.js.map packages/lib/JoplinServerApi.d.ts packages/lib/JoplinServerApi.js packages/lib/JoplinServerApi.js.map -packages/lib/JoplinServerApi2.d.ts -packages/lib/JoplinServerApi2.js -packages/lib/JoplinServerApi2.js.map packages/lib/Logger.d.ts packages/lib/Logger.js packages/lib/Logger.js.map @@ -817,6 +823,9 @@ packages/lib/commands/historyForward.js.map packages/lib/commands/synchronize.d.ts packages/lib/commands/synchronize.js packages/lib/commands/synchronize.js.map +packages/lib/database.d.ts +packages/lib/database.js +packages/lib/database.js.map packages/lib/dummy.test.d.ts packages/lib/dummy.test.js packages/lib/dummy.test.js.map @@ -829,6 +838,9 @@ packages/lib/eventManager.js.map packages/lib/file-api-driver-joplinServer.d.ts packages/lib/file-api-driver-joplinServer.js packages/lib/file-api-driver-joplinServer.js.map +packages/lib/file-api.d.ts +packages/lib/file-api.js +packages/lib/file-api.js.map packages/lib/fs-driver-base.d.ts packages/lib/fs-driver-base.js packages/lib/fs-driver-base.js.map @@ -916,6 +928,9 @@ packages/lib/path-utils.js.map packages/lib/reducer.d.ts packages/lib/reducer.js packages/lib/reducer.js.map +packages/lib/registry.d.ts +packages/lib/registry.js +packages/lib/registry.js.map packages/lib/services/AlarmService.d.ts packages/lib/services/AlarmService.js packages/lib/services/AlarmService.js.map @@ -1432,219 +1447,9 @@ packages/renderer/pathUtils.js.map packages/renderer/utils.d.ts packages/renderer/utils.js packages/renderer/utils.js.map -packages/server/src/app.d.ts -packages/server/src/app.js -packages/server/src/app.js.map -packages/server/src/config.d.ts -packages/server/src/config.js -packages/server/src/config.js.map -packages/server/src/db.d.ts -packages/server/src/db.js -packages/server/src/db.js.map -packages/server/src/middleware/notificationHandler.d.ts -packages/server/src/middleware/notificationHandler.js -packages/server/src/middleware/notificationHandler.js.map -packages/server/src/middleware/notificationHandler.test.d.ts -packages/server/src/middleware/notificationHandler.test.js -packages/server/src/middleware/notificationHandler.test.js.map -packages/server/src/middleware/ownerHandler.d.ts -packages/server/src/middleware/ownerHandler.js -packages/server/src/middleware/ownerHandler.js.map -packages/server/src/middleware/ownerHandler.test.d.ts -packages/server/src/middleware/ownerHandler.test.js -packages/server/src/middleware/ownerHandler.test.js.map -packages/server/src/middleware/routeHandler.d.ts -packages/server/src/middleware/routeHandler.js -packages/server/src/middleware/routeHandler.js.map -packages/server/src/migrations/20190913171451_create.d.ts -packages/server/src/migrations/20190913171451_create.js -packages/server/src/migrations/20190913171451_create.js.map -packages/server/src/migrations/20203012152842_notifications.d.ts -packages/server/src/migrations/20203012152842_notifications.js -packages/server/src/migrations/20203012152842_notifications.js.map -packages/server/src/models/ApiClientModel.d.ts -packages/server/src/models/ApiClientModel.js -packages/server/src/models/ApiClientModel.js.map -packages/server/src/models/BaseModel.d.ts -packages/server/src/models/BaseModel.js -packages/server/src/models/BaseModel.js.map -packages/server/src/models/ChangeModel.d.ts -packages/server/src/models/ChangeModel.js -packages/server/src/models/ChangeModel.js.map -packages/server/src/models/ChangeModel.test.d.ts -packages/server/src/models/ChangeModel.test.js -packages/server/src/models/ChangeModel.test.js.map -packages/server/src/models/FileModel.d.ts -packages/server/src/models/FileModel.js -packages/server/src/models/FileModel.js.map -packages/server/src/models/FileModel.test.d.ts -packages/server/src/models/FileModel.test.js -packages/server/src/models/FileModel.test.js.map -packages/server/src/models/NotificationModel.d.ts -packages/server/src/models/NotificationModel.js -packages/server/src/models/NotificationModel.js.map -packages/server/src/models/NotificationModel.test.d.ts -packages/server/src/models/NotificationModel.test.js -packages/server/src/models/NotificationModel.test.js.map -packages/server/src/models/PermissionModel.d.ts -packages/server/src/models/PermissionModel.js -packages/server/src/models/PermissionModel.js.map -packages/server/src/models/SessionModel.d.ts -packages/server/src/models/SessionModel.js -packages/server/src/models/SessionModel.js.map -packages/server/src/models/UserModel.d.ts -packages/server/src/models/UserModel.js -packages/server/src/models/UserModel.js.map -packages/server/src/models/UserModel.test.d.ts -packages/server/src/models/UserModel.test.js -packages/server/src/models/UserModel.test.js.map -packages/server/src/models/factory.d.ts -packages/server/src/models/factory.js -packages/server/src/models/factory.js.map -packages/server/src/models/utils/pagination.d.ts -packages/server/src/models/utils/pagination.js -packages/server/src/models/utils/pagination.js.map -packages/server/src/models/utils/pagination.test.d.ts -packages/server/src/models/utils/pagination.test.js -packages/server/src/models/utils/pagination.test.js.map -packages/server/src/routes/api/files.d.ts -packages/server/src/routes/api/files.js -packages/server/src/routes/api/files.js.map -packages/server/src/routes/api/files.test.d.ts -packages/server/src/routes/api/files.test.js -packages/server/src/routes/api/files.test.js.map -packages/server/src/routes/api/ping.d.ts -packages/server/src/routes/api/ping.js -packages/server/src/routes/api/ping.js.map -packages/server/src/routes/api/ping.test.d.ts -packages/server/src/routes/api/ping.test.js -packages/server/src/routes/api/ping.test.js.map -packages/server/src/routes/api/sessions.d.ts -packages/server/src/routes/api/sessions.js -packages/server/src/routes/api/sessions.js.map -packages/server/src/routes/api/sessions.test.d.ts -packages/server/src/routes/api/sessions.test.js -packages/server/src/routes/api/sessions.test.js.map -packages/server/src/routes/default.d.ts -packages/server/src/routes/default.js -packages/server/src/routes/default.js.map -packages/server/src/routes/index/files.d.ts -packages/server/src/routes/index/files.js -packages/server/src/routes/index/files.js.map -packages/server/src/routes/index/home.d.ts -packages/server/src/routes/index/home.js -packages/server/src/routes/index/home.js.map -packages/server/src/routes/index/home.test.d.ts -packages/server/src/routes/index/home.test.js -packages/server/src/routes/index/home.test.js.map -packages/server/src/routes/index/login.d.ts -packages/server/src/routes/index/login.js -packages/server/src/routes/index/login.js.map -packages/server/src/routes/index/login.test.d.ts -packages/server/src/routes/index/login.test.js -packages/server/src/routes/index/login.test.js.map -packages/server/src/routes/index/logout.d.ts -packages/server/src/routes/index/logout.js -packages/server/src/routes/index/logout.js.map -packages/server/src/routes/index/logout.test.d.ts -packages/server/src/routes/index/logout.test.js -packages/server/src/routes/index/logout.test.js.map -packages/server/src/routes/index/notifications.d.ts -packages/server/src/routes/index/notifications.js -packages/server/src/routes/index/notifications.js.map -packages/server/src/routes/index/notifications.test.d.ts -packages/server/src/routes/index/notifications.test.js -packages/server/src/routes/index/notifications.test.js.map -packages/server/src/routes/index/users.d.ts -packages/server/src/routes/index/users.js -packages/server/src/routes/index/users.js.map -packages/server/src/routes/index/users.test.d.ts -packages/server/src/routes/index/users.test.js -packages/server/src/routes/index/users.test.js.map -packages/server/src/routes/oauth2/authorize.d.ts -packages/server/src/routes/oauth2/authorize.js -packages/server/src/routes/oauth2/authorize.js.map -packages/server/src/routes/routes.d.ts -packages/server/src/routes/routes.js -packages/server/src/routes/routes.js.map -packages/server/src/services/MustacheService.d.ts -packages/server/src/services/MustacheService.js -packages/server/src/services/MustacheService.js.map -packages/server/src/tools/db-migrate.d.ts -packages/server/src/tools/db-migrate.js -packages/server/src/tools/db-migrate.js.map -packages/server/src/tools/dbTools.d.ts -packages/server/src/tools/dbTools.js -packages/server/src/tools/dbTools.js.map -packages/server/src/tools/generate-types.d.ts -packages/server/src/tools/generate-types.js -packages/server/src/tools/generate-types.js.map -packages/server/src/utils/Router.d.ts -packages/server/src/utils/Router.js -packages/server/src/utils/Router.js.map -packages/server/src/utils/TransactionHandler.d.ts -packages/server/src/utils/TransactionHandler.js -packages/server/src/utils/TransactionHandler.js.map -packages/server/src/utils/auth.d.ts -packages/server/src/utils/auth.js -packages/server/src/utils/auth.js.map -packages/server/src/utils/base64.d.ts -packages/server/src/utils/base64.js -packages/server/src/utils/base64.js.map -packages/server/src/utils/cache.d.ts -packages/server/src/utils/cache.js -packages/server/src/utils/cache.js.map -packages/server/src/utils/defaultView.d.ts -packages/server/src/utils/defaultView.js -packages/server/src/utils/defaultView.js.map -packages/server/src/utils/errors.d.ts -packages/server/src/utils/errors.js -packages/server/src/utils/errors.js.map -packages/server/src/utils/htmlUtils.d.ts -packages/server/src/utils/htmlUtils.js -packages/server/src/utils/htmlUtils.js.map -packages/server/src/utils/koaIf.d.ts -packages/server/src/utils/koaIf.js -packages/server/src/utils/koaIf.js.map -packages/server/src/utils/requestUtils.d.ts -packages/server/src/utils/requestUtils.js -packages/server/src/utils/requestUtils.js.map -packages/server/src/utils/routeUtils.d.ts -packages/server/src/utils/routeUtils.js -packages/server/src/utils/routeUtils.js.map -packages/server/src/utils/routeUtils.test.d.ts -packages/server/src/utils/routeUtils.test.js -packages/server/src/utils/routeUtils.test.js.map -packages/server/src/utils/testing/apiUtils.d.ts -packages/server/src/utils/testing/apiUtils.js -packages/server/src/utils/testing/apiUtils.js.map -packages/server/src/utils/testing/koa/FakeCookies.d.ts -packages/server/src/utils/testing/koa/FakeCookies.js -packages/server/src/utils/testing/koa/FakeCookies.js.map -packages/server/src/utils/testing/koa/FakeRequest.d.ts -packages/server/src/utils/testing/koa/FakeRequest.js -packages/server/src/utils/testing/koa/FakeRequest.js.map -packages/server/src/utils/testing/koa/FakeResponse.d.ts -packages/server/src/utils/testing/koa/FakeResponse.js -packages/server/src/utils/testing/koa/FakeResponse.js.map -packages/server/src/utils/testing/testRouters.d.ts -packages/server/src/utils/testing/testRouters.js -packages/server/src/utils/testing/testRouters.js.map -packages/server/src/utils/testing/testUtils.d.ts -packages/server/src/utils/testing/testUtils.js -packages/server/src/utils/testing/testUtils.js.map -packages/server/src/utils/time.d.ts -packages/server/src/utils/time.js -packages/server/src/utils/time.js.map -packages/server/src/utils/types.d.ts -packages/server/src/utils/types.js -packages/server/src/utils/types.js.map -packages/server/src/utils/urlUtils.d.ts -packages/server/src/utils/urlUtils.js -packages/server/src/utils/urlUtils.js.map -packages/server/src/utils/uuidgen.d.ts -packages/server/src/utils/uuidgen.js -packages/server/src/utils/uuidgen.js.map +packages/tools/generate-database-types.d.ts +packages/tools/generate-database-types.js +packages/tools/generate-database-types.js.map packages/tools/lerna-add.d.ts packages/tools/lerna-add.js packages/tools/lerna-add.js.map diff --git a/.gitignore b/.gitignore index 10cc07a981..48054820b2 100644 --- a/.gitignore +++ b/.gitignore @@ -84,6 +84,9 @@ packages/app-cli/tests/Synchronizer.resources.js.map packages/app-cli/tests/Synchronizer.revisions.d.ts packages/app-cli/tests/Synchronizer.revisions.js packages/app-cli/tests/Synchronizer.revisions.js.map +packages/app-cli/tests/Synchronizer.sharing.d.ts +packages/app-cli/tests/Synchronizer.sharing.js +packages/app-cli/tests/Synchronizer.sharing.js.map packages/app-cli/tests/Synchronizer.tags.d.ts packages/app-cli/tests/Synchronizer.tags.js packages/app-cli/tests/Synchronizer.tags.js.map @@ -771,15 +774,18 @@ packages/lib/BaseApplication.js.map packages/lib/BaseModel.d.ts packages/lib/BaseModel.js packages/lib/BaseModel.js.map +packages/lib/BaseSyncTarget.d.ts +packages/lib/BaseSyncTarget.js +packages/lib/BaseSyncTarget.js.map packages/lib/InMemoryCache.d.ts packages/lib/InMemoryCache.js packages/lib/InMemoryCache.js.map +packages/lib/JoplinDatabase.d.ts +packages/lib/JoplinDatabase.js +packages/lib/JoplinDatabase.js.map packages/lib/JoplinServerApi.d.ts packages/lib/JoplinServerApi.js packages/lib/JoplinServerApi.js.map -packages/lib/JoplinServerApi2.d.ts -packages/lib/JoplinServerApi2.js -packages/lib/JoplinServerApi2.js.map packages/lib/Logger.d.ts packages/lib/Logger.js packages/lib/Logger.js.map @@ -804,6 +810,9 @@ packages/lib/commands/historyForward.js.map packages/lib/commands/synchronize.d.ts packages/lib/commands/synchronize.js packages/lib/commands/synchronize.js.map +packages/lib/database.d.ts +packages/lib/database.js +packages/lib/database.js.map packages/lib/dummy.test.d.ts packages/lib/dummy.test.js packages/lib/dummy.test.js.map @@ -816,6 +825,9 @@ packages/lib/eventManager.js.map packages/lib/file-api-driver-joplinServer.d.ts packages/lib/file-api-driver-joplinServer.js packages/lib/file-api-driver-joplinServer.js.map +packages/lib/file-api.d.ts +packages/lib/file-api.js +packages/lib/file-api.js.map packages/lib/fs-driver-base.d.ts packages/lib/fs-driver-base.js packages/lib/fs-driver-base.js.map @@ -903,6 +915,9 @@ packages/lib/path-utils.js.map packages/lib/reducer.d.ts packages/lib/reducer.js packages/lib/reducer.js.map +packages/lib/registry.d.ts +packages/lib/registry.js +packages/lib/registry.js.map packages/lib/services/AlarmService.d.ts packages/lib/services/AlarmService.js packages/lib/services/AlarmService.js.map @@ -1419,219 +1434,9 @@ packages/renderer/pathUtils.js.map packages/renderer/utils.d.ts packages/renderer/utils.js packages/renderer/utils.js.map -packages/server/src/app.d.ts -packages/server/src/app.js -packages/server/src/app.js.map -packages/server/src/config.d.ts -packages/server/src/config.js -packages/server/src/config.js.map -packages/server/src/db.d.ts -packages/server/src/db.js -packages/server/src/db.js.map -packages/server/src/middleware/notificationHandler.d.ts -packages/server/src/middleware/notificationHandler.js -packages/server/src/middleware/notificationHandler.js.map -packages/server/src/middleware/notificationHandler.test.d.ts -packages/server/src/middleware/notificationHandler.test.js -packages/server/src/middleware/notificationHandler.test.js.map -packages/server/src/middleware/ownerHandler.d.ts -packages/server/src/middleware/ownerHandler.js -packages/server/src/middleware/ownerHandler.js.map -packages/server/src/middleware/ownerHandler.test.d.ts -packages/server/src/middleware/ownerHandler.test.js -packages/server/src/middleware/ownerHandler.test.js.map -packages/server/src/middleware/routeHandler.d.ts -packages/server/src/middleware/routeHandler.js -packages/server/src/middleware/routeHandler.js.map -packages/server/src/migrations/20190913171451_create.d.ts -packages/server/src/migrations/20190913171451_create.js -packages/server/src/migrations/20190913171451_create.js.map -packages/server/src/migrations/20203012152842_notifications.d.ts -packages/server/src/migrations/20203012152842_notifications.js -packages/server/src/migrations/20203012152842_notifications.js.map -packages/server/src/models/ApiClientModel.d.ts -packages/server/src/models/ApiClientModel.js -packages/server/src/models/ApiClientModel.js.map -packages/server/src/models/BaseModel.d.ts -packages/server/src/models/BaseModel.js -packages/server/src/models/BaseModel.js.map -packages/server/src/models/ChangeModel.d.ts -packages/server/src/models/ChangeModel.js -packages/server/src/models/ChangeModel.js.map -packages/server/src/models/ChangeModel.test.d.ts -packages/server/src/models/ChangeModel.test.js -packages/server/src/models/ChangeModel.test.js.map -packages/server/src/models/FileModel.d.ts -packages/server/src/models/FileModel.js -packages/server/src/models/FileModel.js.map -packages/server/src/models/FileModel.test.d.ts -packages/server/src/models/FileModel.test.js -packages/server/src/models/FileModel.test.js.map -packages/server/src/models/NotificationModel.d.ts -packages/server/src/models/NotificationModel.js -packages/server/src/models/NotificationModel.js.map -packages/server/src/models/NotificationModel.test.d.ts -packages/server/src/models/NotificationModel.test.js -packages/server/src/models/NotificationModel.test.js.map -packages/server/src/models/PermissionModel.d.ts -packages/server/src/models/PermissionModel.js -packages/server/src/models/PermissionModel.js.map -packages/server/src/models/SessionModel.d.ts -packages/server/src/models/SessionModel.js -packages/server/src/models/SessionModel.js.map -packages/server/src/models/UserModel.d.ts -packages/server/src/models/UserModel.js -packages/server/src/models/UserModel.js.map -packages/server/src/models/UserModel.test.d.ts -packages/server/src/models/UserModel.test.js -packages/server/src/models/UserModel.test.js.map -packages/server/src/models/factory.d.ts -packages/server/src/models/factory.js -packages/server/src/models/factory.js.map -packages/server/src/models/utils/pagination.d.ts -packages/server/src/models/utils/pagination.js -packages/server/src/models/utils/pagination.js.map -packages/server/src/models/utils/pagination.test.d.ts -packages/server/src/models/utils/pagination.test.js -packages/server/src/models/utils/pagination.test.js.map -packages/server/src/routes/api/files.d.ts -packages/server/src/routes/api/files.js -packages/server/src/routes/api/files.js.map -packages/server/src/routes/api/files.test.d.ts -packages/server/src/routes/api/files.test.js -packages/server/src/routes/api/files.test.js.map -packages/server/src/routes/api/ping.d.ts -packages/server/src/routes/api/ping.js -packages/server/src/routes/api/ping.js.map -packages/server/src/routes/api/ping.test.d.ts -packages/server/src/routes/api/ping.test.js -packages/server/src/routes/api/ping.test.js.map -packages/server/src/routes/api/sessions.d.ts -packages/server/src/routes/api/sessions.js -packages/server/src/routes/api/sessions.js.map -packages/server/src/routes/api/sessions.test.d.ts -packages/server/src/routes/api/sessions.test.js -packages/server/src/routes/api/sessions.test.js.map -packages/server/src/routes/default.d.ts -packages/server/src/routes/default.js -packages/server/src/routes/default.js.map -packages/server/src/routes/index/files.d.ts -packages/server/src/routes/index/files.js -packages/server/src/routes/index/files.js.map -packages/server/src/routes/index/home.d.ts -packages/server/src/routes/index/home.js -packages/server/src/routes/index/home.js.map -packages/server/src/routes/index/home.test.d.ts -packages/server/src/routes/index/home.test.js -packages/server/src/routes/index/home.test.js.map -packages/server/src/routes/index/login.d.ts -packages/server/src/routes/index/login.js -packages/server/src/routes/index/login.js.map -packages/server/src/routes/index/login.test.d.ts -packages/server/src/routes/index/login.test.js -packages/server/src/routes/index/login.test.js.map -packages/server/src/routes/index/logout.d.ts -packages/server/src/routes/index/logout.js -packages/server/src/routes/index/logout.js.map -packages/server/src/routes/index/logout.test.d.ts -packages/server/src/routes/index/logout.test.js -packages/server/src/routes/index/logout.test.js.map -packages/server/src/routes/index/notifications.d.ts -packages/server/src/routes/index/notifications.js -packages/server/src/routes/index/notifications.js.map -packages/server/src/routes/index/notifications.test.d.ts -packages/server/src/routes/index/notifications.test.js -packages/server/src/routes/index/notifications.test.js.map -packages/server/src/routes/index/users.d.ts -packages/server/src/routes/index/users.js -packages/server/src/routes/index/users.js.map -packages/server/src/routes/index/users.test.d.ts -packages/server/src/routes/index/users.test.js -packages/server/src/routes/index/users.test.js.map -packages/server/src/routes/oauth2/authorize.d.ts -packages/server/src/routes/oauth2/authorize.js -packages/server/src/routes/oauth2/authorize.js.map -packages/server/src/routes/routes.d.ts -packages/server/src/routes/routes.js -packages/server/src/routes/routes.js.map -packages/server/src/services/MustacheService.d.ts -packages/server/src/services/MustacheService.js -packages/server/src/services/MustacheService.js.map -packages/server/src/tools/db-migrate.d.ts -packages/server/src/tools/db-migrate.js -packages/server/src/tools/db-migrate.js.map -packages/server/src/tools/dbTools.d.ts -packages/server/src/tools/dbTools.js -packages/server/src/tools/dbTools.js.map -packages/server/src/tools/generate-types.d.ts -packages/server/src/tools/generate-types.js -packages/server/src/tools/generate-types.js.map -packages/server/src/utils/Router.d.ts -packages/server/src/utils/Router.js -packages/server/src/utils/Router.js.map -packages/server/src/utils/TransactionHandler.d.ts -packages/server/src/utils/TransactionHandler.js -packages/server/src/utils/TransactionHandler.js.map -packages/server/src/utils/auth.d.ts -packages/server/src/utils/auth.js -packages/server/src/utils/auth.js.map -packages/server/src/utils/base64.d.ts -packages/server/src/utils/base64.js -packages/server/src/utils/base64.js.map -packages/server/src/utils/cache.d.ts -packages/server/src/utils/cache.js -packages/server/src/utils/cache.js.map -packages/server/src/utils/defaultView.d.ts -packages/server/src/utils/defaultView.js -packages/server/src/utils/defaultView.js.map -packages/server/src/utils/errors.d.ts -packages/server/src/utils/errors.js -packages/server/src/utils/errors.js.map -packages/server/src/utils/htmlUtils.d.ts -packages/server/src/utils/htmlUtils.js -packages/server/src/utils/htmlUtils.js.map -packages/server/src/utils/koaIf.d.ts -packages/server/src/utils/koaIf.js -packages/server/src/utils/koaIf.js.map -packages/server/src/utils/requestUtils.d.ts -packages/server/src/utils/requestUtils.js -packages/server/src/utils/requestUtils.js.map -packages/server/src/utils/routeUtils.d.ts -packages/server/src/utils/routeUtils.js -packages/server/src/utils/routeUtils.js.map -packages/server/src/utils/routeUtils.test.d.ts -packages/server/src/utils/routeUtils.test.js -packages/server/src/utils/routeUtils.test.js.map -packages/server/src/utils/testing/apiUtils.d.ts -packages/server/src/utils/testing/apiUtils.js -packages/server/src/utils/testing/apiUtils.js.map -packages/server/src/utils/testing/koa/FakeCookies.d.ts -packages/server/src/utils/testing/koa/FakeCookies.js -packages/server/src/utils/testing/koa/FakeCookies.js.map -packages/server/src/utils/testing/koa/FakeRequest.d.ts -packages/server/src/utils/testing/koa/FakeRequest.js -packages/server/src/utils/testing/koa/FakeRequest.js.map -packages/server/src/utils/testing/koa/FakeResponse.d.ts -packages/server/src/utils/testing/koa/FakeResponse.js -packages/server/src/utils/testing/koa/FakeResponse.js.map -packages/server/src/utils/testing/testRouters.d.ts -packages/server/src/utils/testing/testRouters.js -packages/server/src/utils/testing/testRouters.js.map -packages/server/src/utils/testing/testUtils.d.ts -packages/server/src/utils/testing/testUtils.js -packages/server/src/utils/testing/testUtils.js.map -packages/server/src/utils/time.d.ts -packages/server/src/utils/time.js -packages/server/src/utils/time.js.map -packages/server/src/utils/types.d.ts -packages/server/src/utils/types.js -packages/server/src/utils/types.js.map -packages/server/src/utils/urlUtils.d.ts -packages/server/src/utils/urlUtils.js -packages/server/src/utils/urlUtils.js.map -packages/server/src/utils/uuidgen.d.ts -packages/server/src/utils/uuidgen.js -packages/server/src/utils/uuidgen.js.map +packages/tools/generate-database-types.d.ts +packages/tools/generate-database-types.js +packages/tools/generate-database-types.js.map packages/tools/lerna-add.d.ts packages/tools/lerna-add.js packages/tools/lerna-add.js.map diff --git a/README.md b/README.md index f42b74952e..825f9ba5df 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,7 @@ The Web Clipper is a browser extension that allows you to save web pages and scr - [Search Sorting spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/search_sorting.md) - [Server: File URL Format](https://github.com/laurent22/joplin/blob/dev/readme/spec/server_file_url_format.md) - [Server: Delta Sync](https://github.com/laurent22/joplin/blob/dev/readme/spec/server_delta_sync.md) + - [Server: Sharing](https://github.com/laurent22/joplin/blob/dev/readme/spec/server_sharing.md) - Google Summer of Code 2020 diff --git a/packages/app-cli/app/cli-integration-tests.js b/packages/app-cli/app/cli-integration-tests.js index e88e033c9b..c536ace1d9 100644 --- a/packages/app-cli/app/cli-integration-tests.js +++ b/packages/app-cli/app/cli-integration-tests.js @@ -4,7 +4,7 @@ const fs = require('fs-extra'); const Logger = require('@joplin/lib/Logger').default; const { dirname } = require('@joplin/lib/path-utils'); const { DatabaseDriverNode } = require('@joplin/lib/database-driver-node.js'); -const { JoplinDatabase } = require('@joplin/lib/joplin-database.js'); +const JoplinDatabase = require('@joplin/lib/JoplinDatabase').default; const BaseModel = require('@joplin/lib/BaseModel').default; const Folder = require('@joplin/lib/models/Folder').default; const Note = require('@joplin/lib/models/Note').default; diff --git a/packages/app-cli/app/command-apidoc.js b/packages/app-cli/app/command-apidoc.js index 2ef4cb804d..dafc14c451 100644 --- a/packages/app-cli/app/command-apidoc.js +++ b/packages/app-cli/app/command-apidoc.js @@ -4,7 +4,7 @@ const BaseModel = require('@joplin/lib/BaseModel').default; const { toTitleCase } = require('@joplin/lib/string-utils.js'); const { reg } = require('@joplin/lib/registry.js'); const markdownUtils = require('@joplin/lib/markdownUtils').default; -const { Database } = require('@joplin/lib/database.js'); +const Database = require('@joplin/lib/database').default; const shim = require('@joplin/lib/shim').default; class Command extends BaseCommand { diff --git a/packages/app-cli/app/command-set.js b/packages/app-cli/app/command-set.js index 4063f6c724..083c8d0776 100644 --- a/packages/app-cli/app/command-set.js +++ b/packages/app-cli/app/command-set.js @@ -2,7 +2,7 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('@joplin/lib/locale'); const BaseModel = require('@joplin/lib/BaseModel').default; -const { Database } = require('@joplin/lib/database.js'); +const Database = require('@joplin/lib/database').default; const Note = require('@joplin/lib/models/Note').default; class Command extends BaseCommand { diff --git a/packages/app-cli/tests/Synchronizer.sharing.ts b/packages/app-cli/tests/Synchronizer.sharing.ts new file mode 100644 index 0000000000..d29acc13a8 --- /dev/null +++ b/packages/app-cli/tests/Synchronizer.sharing.ts @@ -0,0 +1,39 @@ +import { afterAllCleanUp, synchronizerStart, setupDatabaseAndSynchronizer, switchClient } from './test-utils'; +import Note from '@joplin/lib/models/Note'; +import BaseItem from '@joplin/lib/models/BaseItem'; +import shim from '@joplin/lib/shim'; +import Resource from '@joplin/lib/models/Resource'; + +describe('Synchronizer.sharing', function() { + + beforeEach(async (done) => { + await setupDatabaseAndSynchronizer(1); + await setupDatabaseAndSynchronizer(2); + await switchClient(1); + done(); + }); + + afterAll(async () => { + await afterAllCleanUp(); + }); + + it('should mark link resources as shared before syncing', (async () => { + let note1 = await Note.save({ title: 'note1' }); + note1 = await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`); + const resourceId1 = (await Note.linkedResourceIds(note1.body))[0]; + + const note2 = await Note.save({ title: 'note2' }); + await shim.attachFileToNote(note2, `${__dirname}/../tests/support/photo.jpg`); + + expect((await Resource.sharedResourceIds()).length).toBe(0); + + await BaseItem.updateShareStatus(note1, true); + + await synchronizerStart(); + + const sharedResourceIds = await Resource.sharedResourceIds(); + expect(sharedResourceIds.length).toBe(1); + expect(sharedResourceIds[0]).toBe(resourceId1); + })); + +}); diff --git a/packages/app-cli/tests/services_EncryptionService.js b/packages/app-cli/tests/services_EncryptionService.js index f9d0a2fa29..d7d8ea3ff0 100644 --- a/packages/app-cli/tests/services_EncryptionService.js +++ b/packages/app-cli/tests/services_EncryptionService.js @@ -6,7 +6,7 @@ const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synch const Folder = require('@joplin/lib/models/Folder').default; const Note = require('@joplin/lib/models/Note').default; const Tag = require('@joplin/lib/models/Tag').default; -const { Database } = require('@joplin/lib/database.js'); +const Database = require('@joplin/lib/database').default; const Setting = require('@joplin/lib/models/Setting').default; const BaseItem = require('@joplin/lib/models/BaseItem').default; const BaseModel = require('@joplin/lib/BaseModel').default; diff --git a/packages/app-cli/tests/test-utils.ts b/packages/app-cli/tests/test-utils.ts index 1839e38ffd..88ae44bdf0 100644 --- a/packages/app-cli/tests/test-utils.ts +++ b/packages/app-cli/tests/test-utils.ts @@ -18,9 +18,9 @@ import PluginService from '@joplin/lib/services/plugins/PluginService'; import FileApiDriverJoplinServer from '@joplin/lib/file-api-driver-joplinServer'; import OneDriveApi from '@joplin/lib/onedrive-api'; import SyncTargetOneDrive from '@joplin/lib/SyncTargetOneDrive'; +import JoplinDatabase from '@joplin/lib/JoplinDatabase'; const fs = require('fs-extra'); -const { JoplinDatabase } = require('@joplin/lib/joplin-database.js'); const { DatabaseDriverNode } = require('@joplin/lib/database-driver-node.js'); import Folder from '@joplin/lib/models/Folder'; import Note from '@joplin/lib/models/Note'; @@ -52,7 +52,7 @@ import RevisionService from '@joplin/lib/services/RevisionService'; import ResourceFetcher from '@joplin/lib/services/ResourceFetcher'; const WebDavApi = require('@joplin/lib/WebDavApi'); const DropboxApi = require('@joplin/lib/DropboxApi'); -import JoplinServerApi from '@joplin/lib/JoplinServerApi2'; +import JoplinServerApi from '@joplin/lib/JoplinServerApi'; const { loadKeychainServiceAndSettings } = require('@joplin/lib/services/SettingUtils'); const md5 = require('md5'); const S3 = require('aws-sdk/clients/s3'); @@ -402,7 +402,7 @@ async function setupDatabaseAndSynchronizer(id: number, options: any = null) { await fileApi().clearRoot(); } -function db(id: number = null) { +function db(id: number = null): JoplinDatabase { if (id === null) id = currentClient_; return databases_[id]; } diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index 407feb6f68..4789d68738 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -31,7 +31,7 @@ import MasterKey from '@joplin/lib/models/MasterKey'; import Folder from '@joplin/lib/models/Folder'; const fs = require('fs-extra'); import Tag from '@joplin/lib/models/Tag'; -const { reg } = require('@joplin/lib/registry.js'); +import { reg } from '@joplin/lib/registry'; const packageInfo = require('./packageInfo.js'); import DecryptionWorker from '@joplin/lib/services/DecryptionWorker'; const ClipperServer = require('@joplin/lib/ClipperServer'); @@ -704,7 +704,7 @@ class Application extends BaseApplication { if (Setting.value('env') === 'dev') { void AlarmService.updateAllNotifications(); } else { - reg.scheduleSync(1000).then(() => { + void reg.scheduleSync(1000).then(() => { // Wait for the first sync before updating the notifications, since synchronisation // might change the notifications. void AlarmService.updateAllNotifications(); diff --git a/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx b/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx index 5c2a8061ec..90ba1d6bec 100644 --- a/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx +++ b/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx @@ -42,7 +42,7 @@ class ConfigScreenComponent extends React.Component { this.sidebar_selectionChange = this.sidebar_selectionChange.bind(this); this.checkSyncConfig_ = this.checkSyncConfig_.bind(this); - this.checkNextcloudAppButton_click = this.checkNextcloudAppButton_click.bind(this); + // this.checkNextcloudAppButton_click = this.checkNextcloudAppButton_click.bind(this); this.showLogButton_click = this.showLogButton_click.bind(this); this.nextcloudAppHelpLink_click = this.nextcloudAppHelpLink_click.bind(this); this.onCancelClick = this.onCancelClick.bind(this); @@ -57,10 +57,10 @@ class ConfigScreenComponent extends React.Component { await shared.checkSyncConfig(this, this.state.settings); } - async checkNextcloudAppButton_click() { - this.setState({ showNextcloudAppLog: true }); - await shared.checkNextcloudApp(this, this.state.settings); - } + // async checkNextcloudAppButton_click() { + // this.setState({ showNextcloudAppLog: true }); + // await shared.checkNextcloudApp(this, this.state.settings); + // } showLogButton_click() { this.setState({ showNextcloudAppLog: true }); @@ -203,48 +203,48 @@ class ConfigScreenComponent extends React.Component { ); } - if (syncTargetMd.name === 'nextcloud') { - const syncTarget = settings['sync.5.syncTargets'][settings['sync.5.path']]; + // if (syncTargetMd.name === 'nextcloud') { + // const syncTarget = settings['sync.5.syncTargets'][settings['sync.5.path']]; - let status = _('Unknown'); - let errorMessage = null; + // let status = _('Unknown'); + // let errorMessage = null; - if (this.state.checkNextcloudAppResult === 'checking') { - status = _('Checking...'); - } else if (syncTarget) { - if (syncTarget.uuid) status = _('OK'); - if (syncTarget.error) { - status = _('Error'); - errorMessage = syncTarget.error; - } - } + // if (this.state.checkNextcloudAppResult === 'checking') { + // status = _('Checking...'); + // } else if (syncTarget) { + // if (syncTarget.uuid) status = _('OK'); + // if (syncTarget.error) { + // status = _('Error'); + // errorMessage = syncTarget.error; + // } + // } - const statusComp = !errorMessage || this.state.checkNextcloudAppResult === 'checking' || !this.state.showNextcloudAppLog ? null : ( -
-

{_('The Joplin Nextcloud App is either not installed or misconfigured. Please see the full error message below:')}

-
{errorMessage}
-
- ); + // const statusComp = !errorMessage || this.state.checkNextcloudAppResult === 'checking' || !this.state.showNextcloudAppLog ? null : ( + //
+ //

{_('The Joplin Nextcloud App is either not installed or misconfigured. Please see the full error message below:')}

+ //
{errorMessage}
+ //
+ // ); - const showLogButton = !errorMessage || this.state.showNextcloudAppLog ? null : ( - [{_('Show Log')}] - ); + // const showLogButton = !errorMessage || this.state.showNextcloudAppLog ? null : ( + // [{_('Show Log')}] + // ); - const appStatusStyle = Object.assign({}, theme.textStyle, { fontWeight: 'bold' }); + // const appStatusStyle = Object.assign({}, theme.textStyle, { fontWeight: 'bold' }); - settingComps.push( -
- Beta: {_('Joplin Nextcloud App status:')} {status} -    - {showLogButton} -    -
- ); - } + // settingComps.push( + //
+ // Beta: {_('Joplin Nextcloud App status:')} {status} + //    + // {showLogButton} + //    + //
+ // ); + // } } let advancedSettingsButton = null; diff --git a/packages/app-desktop/gui/MenuBar.tsx b/packages/app-desktop/gui/MenuBar.tsx index e4e1adc985..9027d16ab9 100644 --- a/packages/app-desktop/gui/MenuBar.tsx +++ b/packages/app-desktop/gui/MenuBar.tsx @@ -19,7 +19,7 @@ import stateToWhenClauseContext from '../services/commands/stateToWhenClauseCont import bridge from '../services/bridge'; const { connect } = require('react-redux'); -const { reg } = require('@joplin/lib/registry.js'); +import { reg } from '@joplin/lib/registry'; const packageInfo = require('../packageInfo.js'); const { clipboard } = require('electron'); const Menu = bridge().Menu; diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx index 2d3f846f8b..a66f3537e1 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx @@ -30,7 +30,7 @@ const { clipboard } = require('electron'); const shared = require('@joplin/lib/components/shared/note-screen-shared.js'); const Menu = bridge().Menu; const MenuItem = bridge().MenuItem; -const { reg } = require('@joplin/lib/registry.js'); +import { reg } from '@joplin/lib/registry'; const menuUtils = new MenuUtils(CommandService.instance()); @@ -371,7 +371,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) { /* These must be important to prevent the codemirror defaults from taking over*/ .CodeMirror { font-family: monospace; - font-size: ${theme.editorFontSize}px; + font-size: ${props.fontSize}px; height: 100% !important; width: 100% !important; color: inherit !important; diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Editor.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Editor.tsx index 7c9004441f..b113432236 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Editor.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Editor.tsx @@ -31,7 +31,7 @@ import Setting from '@joplin/lib/models/Setting'; // import eventManager from '@joplin/lib/eventManager'; -const { reg } = require('@joplin/lib/registry.js'); +import { reg } from '@joplin/lib/registry'; // Based on http://pypl.github.io/PYPL.html const topLanguages = [ diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/styles/index.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/styles/index.ts index 4e09020975..990f37b62b 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/styles/index.ts +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/styles/index.ts @@ -2,7 +2,7 @@ import { NoteBodyEditorProps } from '../../../utils/types'; const { buildStyle } = require('@joplin/lib/theme'); export default function styles(props: NoteBodyEditorProps) { - return buildStyle('CodeMirror', props.themeId, (theme: any) => { + return buildStyle(['CodeMirror', props.fontSize], props.themeId, (theme: any) => { return { root: { position: 'relative', @@ -49,8 +49,8 @@ export default function styles(props: NoteBodyEditorProps) { flex: 1, overflowY: 'hidden', paddingTop: 0, - lineHeight: `${theme.textAreaLineHeight}px`, - fontSize: `${theme.editorFontSize}px`, + lineHeight: `${Math.round(17 * props.fontSize / 12)}px`, + fontSize: `${props.fontSize}px`, color: theme.color, backgroundColor: theme.backgroundColor, codeMirrorTheme: theme.codeMirrorTheme, // Defined in theme.js diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useExternalPlugins.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useExternalPlugins.ts index 766483c708..e2a5f6bf3b 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useExternalPlugins.ts +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useExternalPlugins.ts @@ -5,7 +5,7 @@ import { extname } from 'path'; import shim from '@joplin/lib/shim'; import uuid from '@joplin/lib/uuid'; -const { reg } = require('@joplin/lib/registry.js'); +import { reg } from '@joplin/lib/registry'; export default function useExternalPlugins(CodeMirror: any, plugins: PluginStates) { diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useKeymap.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useKeymap.ts index a5bb2282a3..1a4613117b 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useKeymap.ts +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useKeymap.ts @@ -3,7 +3,7 @@ import CommandService from '@joplin/lib/services/CommandService'; import KeymapService, { KeymapItem } from '@joplin/lib/services/KeymapService'; import { EditorCommand } from '../../../utils/types'; import shim from '@joplin/lib/shim'; -const { reg } = require('@joplin/lib/registry.js'); +import { reg } from '@joplin/lib/registry'; export default function useKeymap(CodeMirror: any) { diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx index adbf3d1e13..1bdfc27e1b 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx @@ -16,7 +16,7 @@ import shim from '@joplin/lib/shim'; const { MarkupToHtml } = require('@joplin/renderer'); const taboverride = require('taboverride'); -const { reg } = require('@joplin/lib/registry.js'); +import { reg } from '@joplin/lib/registry'; import BaseItem from '@joplin/lib/models/BaseItem'; const { themeStyle } = require('@joplin/lib/theme'); const { clipboard } = require('electron'); diff --git a/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx b/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx index c18ff4871c..e12bb8b578 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx @@ -34,7 +34,7 @@ import ExternalEditWatcher from '@joplin/lib/services/ExternalEditWatcher'; const { themeStyle } = require('@joplin/lib/theme'); const { substrWithEllipsis } = require('@joplin/lib/string-utils'); const NoteSearchBar = require('../NoteSearchBar.min.js'); -const { reg } = require('@joplin/lib/registry.js'); +import { reg } from '@joplin/lib/registry'; import Note from '@joplin/lib/models/Note'; import Folder from '@joplin/lib/models/Folder'; const bridge = require('electron').remote.require('./bridge').default; @@ -399,6 +399,7 @@ function NoteEditor(props: NoteEditorProps) { onDrop: onDrop, noteToolbarButtonInfos: props.toolbarButtonInfos, plugins: props.plugins, + fontSize: Setting.value('style.editor.fontSize'), }; let editor = null; diff --git a/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.ts b/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.ts index 1f1682f201..832b8ed3ca 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.ts @@ -5,7 +5,7 @@ import BaseModel from '@joplin/lib/BaseModel'; import Resource from '@joplin/lib/models/Resource'; const bridge = require('electron').remote.require('./bridge').default; import ResourceFetcher from '@joplin/lib/services/ResourceFetcher'; -const { reg } = require('@joplin/lib/registry.js'); +import { reg } from '@joplin/lib/registry'; const joplinRendererUtils = require('@joplin/renderer').utils; const { clipboard } = require('electron'); const mimeUtils = require('@joplin/lib/mime-utils.js').mime; diff --git a/packages/app-desktop/gui/NoteEditor/utils/types.ts b/packages/app-desktop/gui/NoteEditor/utils/types.ts index 837048e690..dbf21434d6 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/types.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/types.ts @@ -64,6 +64,7 @@ export interface NoteBodyEditorProps { onDrop: Function; noteToolbarButtonInfos: ToolbarButtonInfo[]; plugins: PluginStates; + fontSize: number; } export interface FormNote { diff --git a/packages/app-desktop/gui/NoteEditor/utils/useFormNote.ts b/packages/app-desktop/gui/NoteEditor/utils/useFormNote.ts index 923a9e5495..25f51f3030 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/useFormNote.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/useFormNote.ts @@ -10,7 +10,7 @@ import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index' const { MarkupToHtml } = require('@joplin/renderer'); import Note from '@joplin/lib/models/Note'; -const { reg } = require('@joplin/lib/registry.js'); +import { reg } from '@joplin/lib/registry'; import ResourceFetcher from '@joplin/lib/services/ResourceFetcher'; import DecryptionWorker from '@joplin/lib/services/DecryptionWorker'; diff --git a/packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.ts b/packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.ts index c146b0d6f5..894397a4ff 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.ts @@ -12,7 +12,7 @@ const bridge = require('electron').remote.require('./bridge').default; const { urlDecode } = require('@joplin/lib/string-utils'); const urlUtils = require('@joplin/lib/urlUtils'); import ResourceFetcher from '@joplin/lib/services/ResourceFetcher'; -const { reg } = require('@joplin/lib/registry.js'); +import { reg } from '@joplin/lib/registry'; export default function useMessageHandler(scrollWhenReady: any, setScrollWhenReady: Function, editorRef: any, setLocalSearchResultCount: Function, dispatch: Function, formNote: FormNote) { return useCallback(async (event: any) => { diff --git a/packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.ts b/packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.ts index f0f08f8d5d..5fdd821430 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.ts @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import { FormNote, ScrollOptionTypes } from './types'; import CommandService, { CommandDeclaration, CommandRuntime, CommandContext } from '@joplin/lib/services/CommandService'; import time from '@joplin/lib/time'; -const { reg } = require('@joplin/lib/registry.js'); +import { reg } from '@joplin/lib/registry'; const commandsWithDependencies = [ require('../commands/showLocalSearch'), diff --git a/packages/app-desktop/gui/NoteTextViewer.tsx b/packages/app-desktop/gui/NoteTextViewer.tsx index aa7aa32b0e..57dbaa5951 100644 --- a/packages/app-desktop/gui/NoteTextViewer.tsx +++ b/packages/app-desktop/gui/NoteTextViewer.tsx @@ -1,7 +1,7 @@ import PostMessageService, { MessageResponse, ResponderComponentType } from '@joplin/lib/services/PostMessageService'; import * as React from 'react'; const { connect } = require('react-redux'); -const { reg } = require('@joplin/lib/registry.js'); +import { reg } from '@joplin/lib/registry'; interface Props { onDomReady: Function; diff --git a/packages/app-desktop/gui/OneDriveLoginScreen.tsx b/packages/app-desktop/gui/OneDriveLoginScreen.tsx index 524f42f296..0a7c841f01 100644 --- a/packages/app-desktop/gui/OneDriveLoginScreen.tsx +++ b/packages/app-desktop/gui/OneDriveLoginScreen.tsx @@ -3,7 +3,7 @@ import ButtonBar from './ConfigScreen/ButtonBar'; import { _ } from '@joplin/lib/locale'; const { connect } = require('react-redux'); -const { reg } = require('@joplin/lib/registry.js'); +import { reg } from '@joplin/lib/registry'; import Setting from '@joplin/lib/models/Setting'; const bridge = require('electron').remote.require('./bridge').default; const { themeStyle } = require('@joplin/lib/theme'); @@ -44,7 +44,7 @@ class OneDriveLoginScreenComponent extends React.Component { if (!auth) { log(_('Authentication was not completed (did not receive an authentication token).')); } else { - reg.scheduleSync(0); + void reg.scheduleSync(0); } } diff --git a/packages/app-desktop/gui/ShareNoteDialog.tsx b/packages/app-desktop/gui/ShareNoteDialog.tsx index 068bb2ded6..507a2af8f4 100644 --- a/packages/app-desktop/gui/ShareNoteDialog.tsx +++ b/packages/app-desktop/gui/ShareNoteDialog.tsx @@ -1,14 +1,15 @@ import * as React from 'react'; import { useState, useEffect } from 'react'; import JoplinServerApi from '@joplin/lib/JoplinServerApi'; - import { _, _n } from '@joplin/lib/locale'; -const { themeStyle, buildStyle } = require('@joplin/lib/theme'); -const DialogButtonRow = require('./DialogButtonRow.min'); import Note from '@joplin/lib/models/Note'; import Setting from '@joplin/lib/models/Setting'; import BaseItem from '@joplin/lib/models/BaseItem'; -const { reg } = require('@joplin/lib/registry.js'); +import SyncTargetJoplinServer from '@joplin/lib/SyncTargetJoplinServer'; + +const { themeStyle, buildStyle } = require('@joplin/lib/theme'); +const DialogButtonRow = require('./DialogButtonRow.min'); +import { reg } from '@joplin/lib/registry'; const { clipboard } = require('electron'); interface ShareNoteDialogProps { @@ -82,17 +83,22 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) { void fetchNotes(); }, [props.noteIds]); - const appApi = async () => { - return reg.syncTargetNextcloud().appApi(); + const fileApi = async () => { + const syncTarget = reg.syncTarget() as SyncTargetJoplinServer; + return syncTarget.fileApi(); + }; + + const joplinServerApi = async (): Promise => { + return (await fileApi()).driver().api(); }; const buttonRow_click = () => { props.onClose(); }; - const copyLinksToClipboard = (shares: SharesMap) => { + const copyLinksToClipboard = (api: JoplinServerApi, shares: SharesMap) => { const links = []; - for (const n in shares) links.push(shares[n]._url); + for (const n in shares) links.push(api.shareUrl(shares[n])); clipboard.writeText(links.join('\n')); }; @@ -110,17 +116,15 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) { setSharesState('creating'); - const api = await appApi(); - const syncTargetId = api.syncTargetId(Setting.toPlainObject()); + const api = await joplinServerApi(); + const newShares = Object.assign({}, shares); let sharedStatusChanged = false; for (const note of notes) { - const result = await api.exec('POST', 'shares', { - syncTargetId: syncTargetId, - noteId: note.id, - }); - newShares[note.id] = result; + const fullPath = (await fileApi()).fullPath(BaseItem.systemPath(note.id)); + const share = await api.shareFile(fullPath); + newShares[note.id] = share; const changed = await BaseItem.updateShareStatus(note, true); if (changed) sharedStatusChanged = true; @@ -134,7 +138,7 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) { setSharesState('creating'); } - copyLinksToClipboard(newShares); + copyLinksToClipboard(api, newShares); setSharesState('created'); } catch (error) { @@ -193,7 +197,14 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) { return ''; }; - const encryptionWarningMessage = !Setting.value('encryption.enabled') ? null :
{_('Note: When a note is shared, it will no longer be encrypted on the server.')}
; + function renderEncryptionWarningMessage() { + if (!Setting.value('encryption.enabled')) return null; + return
{_('Note: When a note is shared, it will no longer be encrypted on the server.')}
; + } + + function renderBetaWarningMessage() { + return
{'Sharing notes via Joplin Server is a Beta feature and the API might change later on. What it means is that if you share a note, the link might become invalid after an upgrade, and you will have to share it again.'}
; + } const rootStyle = Object.assign({}, theme.dialogBox); rootStyle.width = '50%'; @@ -205,7 +216,8 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) { {renderNoteList(notes)}
{statusMessage(sharesState)}
- {encryptionWarningMessage} + {renderEncryptionWarningMessage()} + {renderBetaWarningMessage()} diff --git a/packages/app-desktop/gui/utils/NoteListUtils.ts b/packages/app-desktop/gui/utils/NoteListUtils.ts index 709388a18a..5ee81ac50c 100644 --- a/packages/app-desktop/gui/utils/NoteListUtils.ts +++ b/packages/app-desktop/gui/utils/NoteListUtils.ts @@ -1,5 +1,6 @@ import { utils as pluginUtils, PluginStates } from '@joplin/lib/services/plugins/reducer'; import CommandService from '@joplin/lib/services/CommandService'; +import SyncTargetJoplinServer from '@joplin/lib/SyncTargetJoplinServer'; import eventManager from '@joplin/lib/eventManager'; import InteropService from '@joplin/lib/services/interop/InteropService'; import MenuUtils from '@joplin/lib/services/commands/MenuUtils'; @@ -12,6 +13,7 @@ const bridge = require('electron').remote.require('./bridge').default; const Menu = bridge().Menu; const MenuItem = bridge().MenuItem; import Note from '@joplin/lib/models/Note'; +import Setting from '@joplin/lib/models/Setting'; const { substrWithEllipsis } = require('@joplin/lib/string-utils'); interface ContextMenuProps { @@ -131,11 +133,13 @@ export default class NoteListUtils { }) ); - menu.append( - new MenuItem( - menuUtils.commandToStatefulMenuItem('showShareNoteDialog', noteIds.slice()) - ) - ); + if (Setting.value('sync.target') === SyncTargetJoplinServer.id()) { + menu.append( + new MenuItem( + menuUtils.commandToStatefulMenuItem('showShareNoteDialog', noteIds.slice()) + ) + ); + } const exportMenu = new Menu(); diff --git a/packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.tsx b/packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.tsx index 415c1b3704..3dc60e8499 100644 --- a/packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.tsx +++ b/packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.tsx @@ -10,7 +10,7 @@ const { View } = require('react-native'); const { WebView } = require('react-native-webview'); const { themeStyle } = require('../global-style.js'); import BackButtonDialogBox from '../BackButtonDialogBox'; -const { reg } = require('@joplin/lib/registry.js'); +import { reg } from '@joplin/lib/registry'; interface Props { themeId: number; diff --git a/packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.ts b/packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.ts index 64c1bb22bc..51a4289e10 100644 --- a/packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.ts +++ b/packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.ts @@ -4,7 +4,7 @@ import shim from '@joplin/lib/shim'; const { ToastAndroid } = require('react-native'); const { _ } = require('@joplin/lib/locale.js'); -const { reg } = require('@joplin/lib/registry.js'); +import { reg } from '@joplin/lib/registry'; const { dialogs } = require('../../../utils/dialogs.js'); import Resource from '@joplin/lib/models/Resource'; const Share = require('react-native-share').default; diff --git a/packages/app-mobile/components/screens/Note.tsx b/packages/app-mobile/components/screens/Note.tsx index 38614662cb..3f70020ef1 100644 --- a/packages/app-mobile/components/screens/Note.tsx +++ b/packages/app-mobile/components/screens/Note.tsx @@ -29,7 +29,7 @@ const NoteTagsDialog = require('./NoteTagsDialog'); import time from '@joplin/lib/time'; const { Checkbox } = require('../checkbox.js'); const { _ } = require('@joplin/lib/locale'); -const { reg } = require('@joplin/lib/registry.js'); +import { reg } from '@joplin/lib/registry'; import ResourceFetcher from '@joplin/lib/services/ResourceFetcher'; const { BaseScreenComponent } = require('../base-screen.js'); const { themeStyle, editorFont } = require('../global-style.js'); diff --git a/packages/app-mobile/root.tsx b/packages/app-mobile/root.tsx index 51cc82c220..dfdba61fa1 100644 --- a/packages/app-mobile/root.tsx +++ b/packages/app-mobile/root.tsx @@ -50,8 +50,8 @@ import BaseItem from '@joplin/lib/models/BaseItem'; import MasterKey from '@joplin/lib/models/MasterKey'; import Revision from '@joplin/lib/models/Revision'; import RevisionService from '@joplin/lib/services/RevisionService'; -const { JoplinDatabase } = require('@joplin/lib/joplin-database.js'); -const { Database } = require('@joplin/lib/database.js'); +import JoplinDatabase from '@joplin/lib/JoplinDatabase'; +import Database from '@joplin/lib/database'; const { NotesScreen } = require('./components/screens/notes.js'); const { TagsScreen } = require('./components/screens/tags.js'); const { ConfigScreen } = require('./components/screens/config.js'); @@ -67,7 +67,7 @@ const { SideMenu } = require('./components/side-menu.js'); const { SideMenuContent } = require('./components/side-menu-content.js'); const { SideMenuContentNote } = require('./components/side-menu-content-note.js'); const { DatabaseDriverReactNative } = require('./utils/database-driver-react-native'); -const { reg } = require('@joplin/lib/registry.js'); +import { reg } from '@joplin/lib/registry'; const { defaultState } = require('@joplin/lib/reducer'); const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local.js'); import ResourceFetcher from '@joplin/lib/services/ResourceFetcher'; @@ -118,7 +118,7 @@ const generalMiddleware = (store: any) => (next: any) => async (action: any) => if (action.type == 'NAV_GO') Keyboard.dismiss(); if (['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) { - if (!await reg.syncTarget().syncStarted()) reg.scheduleSync(5 * 1000, { syncSteps: ['update_remote', 'delete_remote'] }); + if (!await reg.syncTarget().syncStarted()) void reg.scheduleSync(5 * 1000, { syncSteps: ['update_remote', 'delete_remote'] }); SearchEngine.instance().scheduleSyncTables(); } @@ -151,7 +151,7 @@ const generalMiddleware = (store: any) => (next: any) => async (action: any) => // Schedule a sync operation so that items that need to be encrypted // are sent to sync target. - reg.scheduleSync(); + void reg.scheduleSync(); } if (action.type == 'NAV_GO' && action.routeName == 'Notes') { @@ -427,7 +427,7 @@ async function initialize(dispatch: Function) { db.setLogger(dbLogger); reg.setDb(db); - reg.dispatch = dispatch; + // reg.dispatch = dispatch; BaseModel.dispatch = dispatch; FoldersScreenUtils.dispatch = dispatch; BaseSyncTarget.dispatch = dispatch; @@ -585,7 +585,7 @@ async function initialize(dispatch: Function) { // When the app starts we want the full sync to // start almost immediately to get the latest data. - reg.scheduleSync(1000).then(() => { + void reg.scheduleSync(1000).then(() => { // Wait for the first sync before updating the notifications, since synchronisation // might change the notifications. void AlarmService.updateAllNotifications(); @@ -672,7 +672,7 @@ class AppComponent extends React.Component { if (this.props.selectedFolderId) { await handleShared(sharedData, this.props.selectedFolderId, this.props.dispatch); } else { - reg.logger.info('Cannot handle share - default folder id is not set'); + reg.logger().info('Cannot handle share - default folder id is not set'); } } } diff --git a/packages/app-mobile/setUpQuickActions.ts b/packages/app-mobile/setUpQuickActions.ts index ec48b67e85..5559ac2ae3 100644 --- a/packages/app-mobile/setUpQuickActions.ts +++ b/packages/app-mobile/setUpQuickActions.ts @@ -5,7 +5,7 @@ import * as QuickActions from 'react-native-quick-actions'; import { _ } from '@joplin/lib/locale'; const { DeviceEventEmitter } = require('react-native'); import Note from '@joplin/lib/models/Note'; -const { reg } = require('@joplin/lib/registry.js'); +import { reg } from '@joplin/lib/registry'; type TData = { type: string; diff --git a/packages/lib/BaseApplication.ts b/packages/lib/BaseApplication.ts index be2b5f4af0..403aa50b19 100644 --- a/packages/lib/BaseApplication.ts +++ b/packages/lib/BaseApplication.ts @@ -11,7 +11,7 @@ import SyncTargetOneDrive from './SyncTargetOneDrive'; const { createStore, applyMiddleware } = require('redux'); const { defaultState, stateUtils } = require('./reducer'); -const { JoplinDatabase } = require('./joplin-database.js'); +import JoplinDatabase from './JoplinDatabase'; const { FoldersScreenUtils } = require('./folders-screen-utils.js'); const { DatabaseDriverNode } = require('./database-driver-node.js'); import BaseModel from './BaseModel'; @@ -20,9 +20,9 @@ import BaseItem from './models/BaseItem'; import Note from './models/Note'; import Tag from './models/Tag'; const { splitCommandString } = require('./string-utils.js'); -const { reg } = require('./registry.js'); +import { reg } from './registry'; import time from './time'; -const BaseSyncTarget = require('./BaseSyncTarget.js'); +import BaseSyncTarget from './BaseSyncTarget'; const reduxSharedMiddleware = require('./components/shared/reduxSharedMiddleware'); const os = require('os'); const fs = require('fs-extra'); @@ -433,7 +433,7 @@ export default class BaseApplication { // Schedule a sync operation so that items that need to be encrypted // are sent to sync target. - reg.scheduleSync(); + void reg.scheduleSync(); } }, 'sync.interval': async () => { @@ -470,7 +470,7 @@ export default class BaseApplication { await reduxSharedMiddleware(store, next, action); if (this.hasGui() && ['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) { - if (!(await reg.syncTarget().syncStarted())) reg.scheduleSync(30 * 1000, { syncSteps: ['update_remote', 'delete_remote'] }); + if (!(await reg.syncTarget().syncStarted())) void reg.scheduleSync(30 * 1000, { syncSteps: ['update_remote', 'delete_remote'] }); SearchEngine.instance().scheduleSyncTables(); } @@ -604,7 +604,7 @@ export default class BaseApplication { this.store_ = createStore(this.reducer, applyMiddleware(this.generalMiddlewareFn())); BaseModel.dispatch = this.store().dispatch; FoldersScreenUtils.dispatch = this.store().dispatch; - reg.dispatch = this.store().dispatch; + // reg.dispatch = this.store().dispatch; BaseSyncTarget.dispatch = this.store().dispatch; DecryptionWorker.instance().dispatch = this.store().dispatch; ResourceFetcher.instance().dispatch = this.store().dispatch; @@ -614,7 +614,7 @@ export default class BaseApplication { this.store_ = null; BaseModel.dispatch = function() {}; FoldersScreenUtils.dispatch = function() {}; - reg.dispatch = function() {}; + // reg.dispatch = function() {}; BaseSyncTarget.dispatch = function() {}; DecryptionWorker.instance().dispatch = function() {}; ResourceFetcher.instance().dispatch = function() {}; @@ -720,8 +720,8 @@ export default class BaseApplication { - reg.setLogger(Logger.create('')); - reg.dispatch = () => {}; + reg.setLogger(Logger.create('') as Logger); + // reg.dispatch = () => {}; BaseService.logger_ = globalLogger; diff --git a/packages/lib/BaseModel.ts b/packages/lib/BaseModel.ts index 61c69e9694..3763ae7f15 100644 --- a/packages/lib/BaseModel.ts +++ b/packages/lib/BaseModel.ts @@ -1,8 +1,9 @@ import paginationToSql from './models/utils/paginationToSql'; -const { Database } = require('./database.js'); +import Database from './database'; import uuid from './uuid'; import time from './time'; +import JoplinDatabase from './JoplinDatabase'; const Mutex = require('async-mutex').Mutex; // New code should make use of this enum @@ -69,7 +70,7 @@ class BaseModel { public static dispatch: Function = function() {}; private static saveMutexes_: any = {}; - private static db_: any; + private static db_: JoplinDatabase; static modelType(): ModelType { throw new Error('Must be overriden'); @@ -631,12 +632,12 @@ class BaseModel { return this.db().exec(`DELETE FROM ${this.tableName()} WHERE id = ?`, [id]); } - static batchDelete(ids: string[], options: any = null) { + static async batchDelete(ids: string[], options: any = null) { if (!ids.length) return; options = this.modOptions(options); const idFieldName = options.idFieldName ? options.idFieldName : 'id'; const sql = `DELETE FROM ${this.tableName()} WHERE ${idFieldName} IN ("${ids.join('","')}")`; - return this.db().exec(sql); + await this.db().exec(sql); } static db() { diff --git a/packages/lib/BaseSyncTarget.js b/packages/lib/BaseSyncTarget.js index 840b6b3aa1..d2c4ba2546 100644 --- a/packages/lib/BaseSyncTarget.js +++ b/packages/lib/BaseSyncTarget.js @@ -1,129 +1,135 @@ -const EncryptionService = require('./services/EncryptionService').default; -const shim = require('./shim').default; - +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const EncryptionService_1 = require("./services/EncryptionService"); +const shim_1 = require("./shim"); +const ResourceService_1 = require("./services/ResourceService"); class BaseSyncTarget { - constructor(db, options = null) { - this.db_ = db; - this.synchronizer_ = null; - this.initState_ = null; - this.logger_ = null; - this.options_ = options; - } - - static supportsConfigCheck() { - return false; - } - - option(name, defaultValue = null) { - return this.options_ && name in this.options_ ? this.options_[name] : defaultValue; - } - - logger() { - return this.logger_; - } - - setLogger(v) { - this.logger_ = v; - } - - db() { - return this.db_; - } - - // If [] is returned it means all platforms are supported - static unsupportedPlatforms() { - return []; - } - - async isAuthenticated() { - return false; - } - - authRouteName() { - return null; - } - - static id() { - throw new Error('id() not implemented'); - } - - // Note: it cannot be called just "name()" because that's a reserved keyword and - // it would throw an obscure error in React Native. - static targetName() { - throw new Error('targetName() not implemented'); - } - - static label() { - throw new Error('label() not implemented'); - } - - async initSynchronizer() { - throw new Error('initSynchronizer() not implemented'); - } - - async initFileApi() { - throw new Error('initFileApi() not implemented'); - } - - async fileApi() { - if (this.fileApi_) return this.fileApi_; - this.fileApi_ = await this.initFileApi(); - return this.fileApi_; - } - - fileApiSync() { - return this.fileApi_; - } - - // Usually each sync target should create and setup its own file API via initFileApi() - // but for testing purposes it might be convenient to provide it here so that multiple - // clients can share and sync to the same file api (see test-utils.js) - setFileApi(v) { - this.fileApi_ = v; - } - - async synchronizer() { - if (this.synchronizer_) return this.synchronizer_; - - if (this.initState_ == 'started') { - // Synchronizer is already being initialized, so wait here till it's done. - return new Promise((resolve, reject) => { - const iid = shim.setInterval(() => { - if (this.initState_ == 'ready') { - shim.clearInterval(iid); - resolve(this.synchronizer_); - } - if (this.initState_ == 'error') { - shim.clearInterval(iid); - reject(new Error('Could not initialise synchroniser')); - } - }, 1000); - }); - } else { - this.initState_ = 'started'; - - try { - this.synchronizer_ = await this.initSynchronizer(); - this.synchronizer_.setLogger(this.logger()); - this.synchronizer_.setEncryptionService(EncryptionService.instance()); - this.synchronizer_.dispatch = BaseSyncTarget.dispatch; - this.initState_ = 'ready'; - return this.synchronizer_; - } catch (error) { - this.initState_ = 'error'; - throw error; - } - } - } - - async syncStarted() { - if (!this.synchronizer_) return false; - if (!(await this.isAuthenticated())) return false; - const sync = await this.synchronizer(); - return sync.state() != 'idle'; - } + constructor(db, options = null) { + this.synchronizer_ = null; + this.initState_ = null; + this.logger_ = null; + this.db_ = db; + this.options_ = options; + } + static supportsConfigCheck() { + return false; + } + option(name, defaultValue = null) { + return this.options_ && name in this.options_ ? this.options_[name] : defaultValue; + } + logger() { + return this.logger_; + } + setLogger(v) { + this.logger_ = v; + } + db() { + return this.db_; + } + // If [] is returned it means all platforms are supported + static unsupportedPlatforms() { + return []; + } + isAuthenticated() { + return __awaiter(this, void 0, void 0, function* () { + return false; + }); + } + authRouteName() { + return null; + } + static id() { + throw new Error('id() not implemented'); + } + // Note: it cannot be called just "name()" because that's a reserved keyword and + // it would throw an obscure error in React Native. + static targetName() { + throw new Error('targetName() not implemented'); + } + static label() { + throw new Error('label() not implemented'); + } + initSynchronizer() { + return __awaiter(this, void 0, void 0, function* () { + throw new Error('initSynchronizer() not implemented'); + }); + } + initFileApi() { + return __awaiter(this, void 0, void 0, function* () { + throw new Error('initFileApi() not implemented'); + }); + } + fileApi() { + return __awaiter(this, void 0, void 0, function* () { + if (this.fileApi_) + return this.fileApi_; + this.fileApi_ = yield this.initFileApi(); + return this.fileApi_; + }); + } + // Usually each sync target should create and setup its own file API via initFileApi() + // but for testing purposes it might be convenient to provide it here so that multiple + // clients can share and sync to the same file api (see test-utils.js) + setFileApi(v) { + this.fileApi_ = v; + } + synchronizer() { + return __awaiter(this, void 0, void 0, function* () { + if (this.synchronizer_) + return this.synchronizer_; + if (this.initState_ == 'started') { + // Synchronizer is already being initialized, so wait here till it's done. + return new Promise((resolve, reject) => { + const iid = shim_1.default.setInterval(() => { + if (this.initState_ == 'ready') { + shim_1.default.clearInterval(iid); + resolve(this.synchronizer_); + } + if (this.initState_ == 'error') { + shim_1.default.clearInterval(iid); + reject(new Error('Could not initialise synchroniser')); + } + }, 1000); + }); + } + else { + this.initState_ = 'started'; + try { + this.synchronizer_ = yield this.initSynchronizer(); + this.synchronizer_.setLogger(this.logger()); + this.synchronizer_.setEncryptionService(EncryptionService_1.default.instance()); + this.synchronizer_.setResourceService(ResourceService_1.default.instance()); + this.synchronizer_.dispatch = BaseSyncTarget.dispatch; + this.initState_ = 'ready'; + return this.synchronizer_; + } + catch (error) { + this.initState_ = 'error'; + throw error; + } + } + }); + } + syncStarted() { + return __awaiter(this, void 0, void 0, function* () { + if (!this.synchronizer_) + return false; + if (!(yield this.isAuthenticated())) + return false; + const sync = yield this.synchronizer(); + return sync.state() != 'idle'; + }); + } } - -BaseSyncTarget.dispatch = () => {}; - -module.exports = BaseSyncTarget; +exports.default = BaseSyncTarget; +BaseSyncTarget.dispatch = () => { }; +//# sourceMappingURL=BaseSyncTarget.js.map \ No newline at end of file diff --git a/packages/lib/BaseSyncTarget.ts b/packages/lib/BaseSyncTarget.ts new file mode 100644 index 0000000000..bd9e8cf3ef --- /dev/null +++ b/packages/lib/BaseSyncTarget.ts @@ -0,0 +1,132 @@ +import Logger from './Logger'; +import Synchronizer from './Synchronizer'; +import EncryptionService from './services/EncryptionService'; +import shim from './shim'; +import ResourceService from './services/ResourceService'; + +export default class BaseSyncTarget { + + public static dispatch: Function = () => {}; + + private synchronizer_: Synchronizer = null; + private initState_: any = null; + private logger_: Logger = null; + private options_: any; + private db_: any; + protected fileApi_: any; + + public constructor(db: any, options: any = null) { + this.db_ = db; + this.options_ = options; + } + + public static supportsConfigCheck() { + return false; + } + + public option(name: string, defaultValue: any = null) { + return this.options_ && name in this.options_ ? this.options_[name] : defaultValue; + } + + protected logger() { + return this.logger_; + } + + public setLogger(v: Logger) { + this.logger_ = v; + } + + protected db() { + return this.db_; + } + + // If [] is returned it means all platforms are supported + public static unsupportedPlatforms(): any[] { + return []; + } + + public async isAuthenticated() { + return false; + } + + public authRouteName(): string { + return null; + } + + public static id() { + throw new Error('id() not implemented'); + } + + // Note: it cannot be called just "name()" because that's a reserved keyword and + // it would throw an obscure error in React Native. + public static targetName() { + throw new Error('targetName() not implemented'); + } + + public static label() { + throw new Error('label() not implemented'); + } + + protected async initSynchronizer(): Promise { + throw new Error('initSynchronizer() not implemented'); + } + + protected async initFileApi(): Promise { + throw new Error('initFileApi() not implemented'); + } + + public async fileApi() { + if (this.fileApi_) return this.fileApi_; + this.fileApi_ = await this.initFileApi(); + return this.fileApi_; + } + + // Usually each sync target should create and setup its own file API via initFileApi() + // but for testing purposes it might be convenient to provide it here so that multiple + // clients can share and sync to the same file api (see test-utils.js) + public setFileApi(v: any) { + this.fileApi_ = v; + } + + public async synchronizer(): Promise { + if (this.synchronizer_) return this.synchronizer_; + + if (this.initState_ == 'started') { + // Synchronizer is already being initialized, so wait here till it's done. + return new Promise((resolve, reject) => { + const iid = shim.setInterval(() => { + if (this.initState_ == 'ready') { + shim.clearInterval(iid); + resolve(this.synchronizer_); + } + if (this.initState_ == 'error') { + shim.clearInterval(iid); + reject(new Error('Could not initialise synchroniser')); + } + }, 1000); + }); + } else { + this.initState_ = 'started'; + + try { + this.synchronizer_ = await this.initSynchronizer(); + this.synchronizer_.setLogger(this.logger()); + this.synchronizer_.setEncryptionService(EncryptionService.instance()); + this.synchronizer_.setResourceService(ResourceService.instance()); + this.synchronizer_.dispatch = BaseSyncTarget.dispatch; + this.initState_ = 'ready'; + return this.synchronizer_; + } catch (error) { + this.initState_ = 'error'; + throw error; + } + } + } + + public async syncStarted() { + if (!this.synchronizer_) return false; + if (!(await this.isAuthenticated())) return false; + const sync = await this.synchronizer(); + return sync.state() != 'idle'; + } +} diff --git a/packages/lib/joplin-database.js b/packages/lib/JoplinDatabase.ts similarity index 96% rename from packages/lib/joplin-database.js rename to packages/lib/JoplinDatabase.ts index 0af941585b..5dd5f325c6 100644 --- a/packages/lib/joplin-database.js +++ b/packages/lib/JoplinDatabase.ts @@ -1,8 +1,9 @@ +import Resource from './models/Resource'; +import shim from './shim'; +import Database, { SqlQuery } from './database'; + const { promiseChain } = require('./promise-utils.js'); -const { Database } = require('./database.js'); const { sprintf } = require('sprintf-js'); -const Resource = require('./models/Resource').default; -const shim = require('./shim').default; const structureSql = ` CREATE TABLE folders ( @@ -118,13 +119,28 @@ CREATE TABLE version ( INSERT INTO version (version) VALUES (1); `; -class JoplinDatabase extends Database { - constructor(driver) { +interface TableField { + name: string; + type: number; + default: any; + description?: string; +} + +export default class JoplinDatabase extends Database { + + public static TYPE_INT = 1; + public static TYPE_TEXT = 2; + public static TYPE_NUMERIC = 3; + + private initialized_ = false; + private tableFields_: Record = null; + private version_: number = null; + private tableFieldNames_: Record = {}; + private tableDescriptions_: any; + + constructor(driver: any) { super(driver); - this.initialized_ = false; - this.tableFields_ = null; - this.version_ = null; - this.tableFieldNames_ = {}; + // this.extensionToLoad = './build/lib/sql-extensions/spellfix'; } @@ -132,12 +148,12 @@ class JoplinDatabase extends Database { return this.initialized_; } - async open(options) { + async open(options: any) { await super.open(options); return this.initialize(); } - tableFieldNames(tableName) { + tableFieldNames(tableName: string) { if (this.tableFieldNames_[tableName]) return this.tableFieldNames_[tableName].slice(); const tf = this.tableFields(tableName); @@ -150,7 +166,7 @@ class JoplinDatabase extends Database { return output.slice(); } - tableFields(tableName, options = null) { + tableFields(tableName: string, options: any = null) { if (options === null) options = {}; if (!this.tableFields_) throw new Error('Fields have not been loaded yet'); @@ -206,9 +222,9 @@ class JoplinDatabase extends Database { await this.transactionExecBatch(queries); } - createDefaultRow() { - const row = {}; - const fields = this.tableFields('resource_local_states'); + createDefaultRow(tableName: string) { + const row: any = {}; + const fields = this.tableFields(tableName); for (let i = 0; i < fields.length; i++) { const f = fields[i]; row[f.name] = Database.formatValue(f.type, f.default); @@ -216,7 +232,7 @@ class JoplinDatabase extends Database { return row; } - fieldByName(tableName, fieldName) { + fieldByName(tableName: string, fieldName: string) { const fields = this.tableFields(tableName); for (const field of fields) { if (field.name === fieldName) return field; @@ -224,11 +240,11 @@ class JoplinDatabase extends Database { throw new Error(`No such field: ${tableName}: ${fieldName}`); } - fieldDefaultValue(tableName, fieldName) { + fieldDefaultValue(tableName: string, fieldName: string) { return this.fieldByName(tableName, fieldName).default; } - fieldDescription(tableName, fieldName) { + fieldDescription(tableName: string, fieldName: string) { const sp = sprintf; if (!this.tableDescriptions_) { @@ -264,9 +280,9 @@ class JoplinDatabase extends Database { return d && d[fieldName] ? d[fieldName] : ''; } - refreshTableFields(newVersion) { + refreshTableFields(newVersion: number) { this.logger().info('Initializing tables...'); - const queries = []; + const queries: SqlQuery[] = []; queries.push(this.wrapQuery('DELETE FROM table_fields')); return this.selectAll('SELECT name FROM sqlite_master WHERE type="table"') @@ -309,12 +325,12 @@ class JoplinDatabase extends Database { }); } - addMigrationFile(num) { + addMigrationFile(num: number) { const timestamp = Date.now(); return { sql: 'INSERT INTO migrations (number, created_time, updated_time) VALUES (?, ?, ?)', params: [num, timestamp, timestamp] }; } - async upgradeDatabase(fromVersion) { + async upgradeDatabase(fromVersion: number) { // INSTRUCTIONS TO UPGRADE THE DATABASE: // // 1. Add the new version number to the existingDatabaseVersions array @@ -353,7 +369,7 @@ class JoplinDatabase extends Database { const targetVersion = existingDatabaseVersions[currentVersionIndex + 1]; this.logger().info(`Converting database to version ${targetVersion}`); - let queries = []; + let queries: any[] = []; if (targetVersion == 1) { queries = this.wrapQueries(this.sqlStringToLines(structureSql)); @@ -965,9 +981,3 @@ class JoplinDatabase extends Database { } } } - -Database.TYPE_INT = 1; -Database.TYPE_TEXT = 2; -Database.TYPE_NUMERIC = 3; - -module.exports = { JoplinDatabase }; diff --git a/packages/lib/JoplinServerApi.ts b/packages/lib/JoplinServerApi.ts index 08bd462c84..f952ec65d0 100644 --- a/packages/lib/JoplinServerApi.ts +++ b/packages/lib/JoplinServerApi.ts @@ -1,163 +1,190 @@ import shim from './shim'; import { _ } from './locale'; -import Logger from './Logger'; +const { rtrimSlashes } = require('./path-utils.js'); const JoplinError = require('./JoplinError'); -const { rtrimSlashes } = require('./path-utils'); -const base64 = require('base-64'); +const { stringify } = require('query-string'); -interface JoplinServerApiOptions { - username: Function; - password: Function; - baseUrl: Function; +interface Options { + baseUrl(): string; + username(): string; + password(): string; +} + +enum ExecOptionsResponseFormat { + Json = 'json', + Text = 'text', +} + +enum ExecOptionsTarget { + String = 'string', + File = 'file', +} + +interface ExecOptions { + responseFormat?: ExecOptionsResponseFormat; + target?: ExecOptionsTarget; + path?: string; + source?: string; } export default class JoplinServerApi { - logger_: any; - options_: JoplinServerApiOptions; - kvStore_: any; + private options_: Options; + private session_: any; - constructor(options: JoplinServerApiOptions) { - this.logger_ = new Logger(); + public constructor(options: Options) { this.options_ = options; - this.kvStore_ = null; } - setLogger(l: any) { - this.logger_ = l; - } - - logger(): any { - return this.logger_; - } - - setKvStore(v: any) { - this.kvStore_ = v; - } - - kvStore() { - if (!this.kvStore_) throw new Error('JoplinServerApi.kvStore_ is not set!!'); - return this.kvStore_; - } - - authToken(): string { - if (!this.options_.username() || !this.options_.password()) return null; - try { - // Note: Non-ASCII passwords will throw an error about Latin1 characters - https://github.com/laurent22/joplin/issues/246 - // Tried various things like the below, but it didn't work on React Native: - // return base64.encode(utf8.encode(this.options_.username() + ':' + this.options_.password())); - return base64.encode(`${this.options_.username()}:${this.options_.password()}`); - } catch (error) { - error.message = `Cannot encode username/password: ${error.message}`; - throw error; - } - } - - baseUrl(): string { + private baseUrl() { return rtrimSlashes(this.options_.baseUrl()); } - static baseUrlFromNextcloudWebDavUrl(webDavUrl: string) { - // http://nextcloud.local/remote.php/webdav/Joplin - // http://nextcloud.local/index.php/apps/joplin/api - const splitted = webDavUrl.split('/remote.php/webdav'); - if (splitted.length !== 2) throw new Error(`Unsupported WebDAV URL format: ${webDavUrl}`); - return `${splitted[0]}/index.php/apps/joplin/api`; + private async session() { + // TODO: handle invalid session + if (this.session_) return this.session_; + + this.session_ = await this.exec('POST', 'api/sessions', null, { + email: this.options_.username(), + password: this.options_.password(), + }); + + return this.session_; } - syncTargetId(settings: any) { - const s = settings['sync.5.syncTargets'][settings['sync.5.path']]; - if (!s) throw new Error(`Joplin Nextcloud app not configured for URL: ${this.baseUrl()}`); - return s.uuid; + private async sessionId() { + const session = await this.session(); + return session ? session.id : ''; } - static connectionErrorMessage(error: any) { - const msg = error && error.message ? error.message : 'Unknown error'; - return _('Could not connect to the Joplin Nextcloud app. Please check the configuration in the Synchronisation config screen. Full error was:\n\n%s', msg); - } - - async setupSyncTarget(webDavUrl: string) { - return this.exec('POST', 'sync_targets', { - webDavUrl: webDavUrl, + public async shareFile(pathOrId: string) { + return this.exec('POST', 'api/shares', null, { + file_id: pathOrId, + type: 1, // ShareType.Link }); } - requestToCurl_(url: string, options: any) { - const output = []; - output.push('curl'); - output.push('-v'); - if (options.method) output.push(`-X ${options.method}`); - if (options.headers) { - for (const n in options.headers) { - if (!options.headers.hasOwnProperty(n)) continue; - output.push(`${'-H ' + '"'}${n}: ${options.headers[n]}"`); - } - } - if (options.body) output.push(`${'--data ' + '\''}${options.body}'`); - output.push(url); - - return output.join(' '); + public static connectionErrorMessage(error: any) { + const msg = error && error.message ? error.message : 'Unknown error'; + return _('Could not connect to Joplin Server. Please check the Synchronisation options in the config screen. Full error was:\n\n%s', msg); } - async exec(method: string, path: string = '', body: any = null, headers: any = null, options: any = null): Promise { + public shareUrl(share: any): string { + return `${this.baseUrl()}/shares/${share.id}`; + } + + // private requestToCurl_(url: string, options: any) { + // const output = []; + // output.push('curl'); + // output.push('-v'); + // if (options.method) output.push(`-X ${options.method}`); + // if (options.headers) { + // for (const n in options.headers) { + // if (!options.headers.hasOwnProperty(n)) continue; + // output.push(`${'-H ' + '"'}${n}: ${options.headers[n]}"`); + // } + // } + // if (options.body) output.push(`${'--data ' + '\''}${JSON.stringify(options.body)}'`); + // output.push(url); + + // return output.join(' '); + // } + + public async exec(method: string, path: string = '', query: Record = null, body: any = null, headers: any = null, options: ExecOptions = null) { if (headers === null) headers = {}; if (options === null) options = {}; + if (!options.responseFormat) options.responseFormat = ExecOptionsResponseFormat.Json; + if (!options.target) options.target = ExecOptionsTarget.String; - const authToken = this.authToken(); + let sessionId = ''; + if (path !== 'api/sessions' && !sessionId) { + sessionId = await this.sessionId(); + } - if (authToken) headers['Authorization'] = `Basic ${authToken}`; - - headers['Content-Type'] = 'application/json'; - - if (typeof body === 'object' && body !== null) body = JSON.stringify(body); + if (sessionId) headers['X-API-AUTH'] = sessionId; const fetchOptions: any = {}; fetchOptions.headers = headers; fetchOptions.method = method; if (options.path) fetchOptions.path = options.path; - if (body) fetchOptions.body = body; - const url = `${this.baseUrl()}/${path}`; + if (body) { + if (typeof body === 'object') { + fetchOptions.body = JSON.stringify(body); + fetchOptions.headers['Content-Type'] = 'application/json'; + } else { + fetchOptions.body = body; + } - let response = null; + fetchOptions.headers['Content-Length'] = `${shim.stringByteLength(fetchOptions.body)}`; + } - // console.info('WebDAV Call', method + ' ' + url, headers, options); - console.info(this.requestToCurl_(url, fetchOptions)); + let url = `${this.baseUrl()}/${path}`; - if (typeof body === 'string') fetchOptions.headers['Content-Length'] = `${shim.stringByteLength(body)}`; - response = await shim.fetch(url, fetchOptions); + if (query) { + url += url.indexOf('?') < 0 ? '?' : '&'; + url += stringify(query); + } + + let response: any = null; + + // console.info('Joplin API Call', `${method} ${url}`, headers, options); + // console.info(this.requestToCurl_(url, fetchOptions)); + + if (options.source == 'file' && (method == 'POST' || method == 'PUT')) { + if (fetchOptions.path) { + const fileStat = await shim.fsDriver().stat(fetchOptions.path); + if (fileStat) fetchOptions.headers['Content-Length'] = `${fileStat.size}`; + } + response = await shim.uploadBlob(url, fetchOptions); + } else if (options.target == 'string') { + if (typeof body === 'string') fetchOptions.headers['Content-Length'] = `${shim.stringByteLength(body)}`; + response = await shim.fetch(url, fetchOptions); + } else { + // file + response = await shim.fetchBlob(url, fetchOptions); + } const responseText = await response.text(); - const responseJson_: any = null; + // console.info('Joplin API Response', responseText); + + // Creates an error object with as much data as possible as it will appear in the log, which will make debugging easier + const newError = (message: string, code: number = 0) => { + // Gives a shorter response for error messages. Useful for cases where a full HTML page is accidentally loaded instead of + // JSON. That way the error message will still show there's a problem but without filling up the log or screen. + const shortResponseText = (`${responseText}`).substr(0, 1024); + return new JoplinError(`${method} ${path}: ${message} (${code}): ${shortResponseText}`, code); + }; + + let responseJson_: any = null; const loadResponseJson = async () => { if (!responseText) return null; if (responseJson_) return responseJson_; - try { - return JSON.parse(responseText); - } catch (error) { - throw new Error(`Cannot parse JSON: ${responseText.substr(0, 8192)}`); - } - }; - - const newError = (message: string, code: number = 0) => { - return new JoplinError(`${method} ${path}: ${message} (${code})`, code); + responseJson_ = JSON.parse(responseText); + if (!responseJson_) throw newError('Cannot parse JSON response', response.status); + return responseJson_; }; if (!response.ok) { + if (options.target === 'file') throw newError('fetchBlob error', response.status); + let json = null; try { json = await loadResponseJson(); } catch (error) { - throw newError(`Unknown error: ${responseText.substr(0, 8192)}`, response.status); + // Just send back the plain text in newErro() } - const trace = json.stacktrace ? `\n${json.stacktrace}` : ''; - let message = json.error; - if (!message) message = responseText.substr(0, 8192); - throw newError(message + trace, response.status); + if (json && json.error) { + throw newError(`${json.error}`, json.code ? json.code : response.status); + } + + throw newError('Unknown error', response.status); } + if (options.responseFormat === 'text') return responseText; + const output = await loadResponseJson(); return output; } diff --git a/packages/lib/JoplinServerApi2.ts b/packages/lib/JoplinServerApi2.ts deleted file mode 100644 index f9afe68924..0000000000 --- a/packages/lib/JoplinServerApi2.ts +++ /dev/null @@ -1,174 +0,0 @@ -import shim from './shim'; -const { rtrimSlashes } = require('./path-utils.js'); -const JoplinError = require('./JoplinError'); -const { stringify } = require('query-string'); - -interface Options { - baseUrl(): string; - username(): string; - password(): string; -} - -enum ExecOptionsResponseFormat { - Json = 'json', - Text = 'text', -} - -enum ExecOptionsTarget { - String = 'string', - File = 'file', -} - -interface ExecOptions { - responseFormat?: ExecOptionsResponseFormat; - target?: ExecOptionsTarget; - path?: string; - source?: string; -} - -export default class JoplinServerApi { - - private options_: Options; - private session_: any; - - public constructor(options: Options) { - this.options_ = options; - } - - private baseUrl() { - return rtrimSlashes(this.options_.baseUrl()); - } - - private async session() { - // TODO: handle invalid session - if (this.session_) return this.session_; - - this.session_ = await this.exec('POST', 'api/sessions', null, { - email: this.options_.username(), - password: this.options_.password(), - }); - - return this.session_; - } - - private async sessionId() { - const session = await this.session(); - return session ? session.id : ''; - } - - // private requestToCurl_(url: string, options: any) { - // const output = []; - // output.push('curl'); - // output.push('-v'); - // if (options.method) output.push(`-X ${options.method}`); - // if (options.headers) { - // for (const n in options.headers) { - // if (!options.headers.hasOwnProperty(n)) continue; - // output.push(`${'-H ' + '"'}${n}: ${options.headers[n]}"`); - // } - // } - // if (options.body) output.push(`${'--data ' + '\''}${JSON.stringify(options.body)}'`); - // output.push(url); - - // return output.join(' '); - // } - - public async exec(method: string, path: string = '', query: Record = null, body: any = null, headers: any = null, options: ExecOptions = null) { - if (headers === null) headers = {}; - if (options === null) options = {}; - if (!options.responseFormat) options.responseFormat = ExecOptionsResponseFormat.Json; - if (!options.target) options.target = ExecOptionsTarget.String; - - let sessionId = ''; - if (path !== 'api/sessions' && !sessionId) { - sessionId = await this.sessionId(); - } - - if (sessionId) headers['X-API-AUTH'] = sessionId; - - const fetchOptions: any = {}; - fetchOptions.headers = headers; - fetchOptions.method = method; - if (options.path) fetchOptions.path = options.path; - - if (body) { - if (typeof body === 'object') { - fetchOptions.body = JSON.stringify(body); - fetchOptions.headers['Content-Type'] = 'application/json'; - } else { - fetchOptions.body = body; - } - - fetchOptions.headers['Content-Length'] = `${shim.stringByteLength(fetchOptions.body)}`; - } - - let url = `${this.baseUrl()}/${path}`; - - if (query) { - url += url.indexOf('?') < 0 ? '?' : '&'; - url += stringify(query); - } - - let response: any = null; - - // console.info('Joplin API Call', `${method} ${url}`, headers, options); - // console.info(this.requestToCurl_(url, fetchOptions)); - - if (options.source == 'file' && (method == 'POST' || method == 'PUT')) { - if (fetchOptions.path) { - const fileStat = await shim.fsDriver().stat(fetchOptions.path); - if (fileStat) fetchOptions.headers['Content-Length'] = `${fileStat.size}`; - } - response = await shim.uploadBlob(url, fetchOptions); - } else if (options.target == 'string') { - if (typeof body === 'string') fetchOptions.headers['Content-Length'] = `${shim.stringByteLength(body)}`; - response = await shim.fetch(url, fetchOptions); - } else { - // file - response = await shim.fetchBlob(url, fetchOptions); - } - - const responseText = await response.text(); - - // console.info('Joplin API Response', responseText); - - // Creates an error object with as much data as possible as it will appear in the log, which will make debugging easier - const newError = (message: string, code: number = 0) => { - // Gives a shorter response for error messages. Useful for cases where a full HTML page is accidentally loaded instead of - // JSON. That way the error message will still show there's a problem but without filling up the log or screen. - const shortResponseText = (`${responseText}`).substr(0, 1024); - return new JoplinError(`${method} ${path}: ${message} (${code}): ${shortResponseText}`, code); - }; - - let responseJson_: any = null; - const loadResponseJson = async () => { - if (!responseText) return null; - if (responseJson_) return responseJson_; - responseJson_ = JSON.parse(responseText); - if (!responseJson_) throw newError('Cannot parse JSON response', response.status); - return responseJson_; - }; - - if (!response.ok) { - if (options.target === 'file') throw newError('fetchBlob error', response.status); - - let json = null; - try { - json = await loadResponseJson(); - } catch (error) { - // Just send back the plain text in newErro() - } - - if (json && json.message) { - throw newError(`${json.message}`, response.status); - } - - throw newError('Unknown error', response.status); - } - - if (options.responseFormat === 'text') return responseText; - - const output = await loadResponseJson(); - return output; - } -} diff --git a/packages/lib/SyncTargetAmazonS3.js b/packages/lib/SyncTargetAmazonS3.js index b21c665c1c..e84ae2a8ae 100644 --- a/packages/lib/SyncTargetAmazonS3.js +++ b/packages/lib/SyncTargetAmazonS3.js @@ -1,4 +1,4 @@ -const BaseSyncTarget = require('./BaseSyncTarget.js'); +const BaseSyncTarget = require('./BaseSyncTarget').default; const { _ } = require('./locale'); const Setting = require('./models/Setting').default; const { FileApi } = require('./file-api.js'); diff --git a/packages/lib/SyncTargetDropbox.js b/packages/lib/SyncTargetDropbox.js index 65d86a0038..cf11844614 100644 --- a/packages/lib/SyncTargetDropbox.js +++ b/packages/lib/SyncTargetDropbox.js @@ -1,4 +1,4 @@ -const BaseSyncTarget = require('./BaseSyncTarget.js'); +const BaseSyncTarget = require('./BaseSyncTarget').default; const { _ } = require('./locale'); const DropboxApi = require('./DropboxApi'); const Setting = require('./models/Setting').default; diff --git a/packages/lib/SyncTargetFilesystem.js b/packages/lib/SyncTargetFilesystem.js index 85ba1f8abd..ec7ce80b13 100644 --- a/packages/lib/SyncTargetFilesystem.js +++ b/packages/lib/SyncTargetFilesystem.js @@ -1,4 +1,4 @@ -const BaseSyncTarget = require('./BaseSyncTarget.js'); +const BaseSyncTarget = require('./BaseSyncTarget').default; const { _ } = require('./locale'); const Setting = require('./models/Setting').default; const { FileApi } = require('./file-api.js'); diff --git a/packages/lib/SyncTargetJoplinServer.ts b/packages/lib/SyncTargetJoplinServer.ts index c756f53020..a12b5e81f1 100644 --- a/packages/lib/SyncTargetJoplinServer.ts +++ b/packages/lib/SyncTargetJoplinServer.ts @@ -2,10 +2,9 @@ import FileApiDriverJoplinServer from './file-api-driver-joplinServer'; import Setting from './models/Setting'; import Synchronizer from './Synchronizer'; import { _ } from './locale.js'; -import JoplinServerApi from './JoplinServerApi2'; - -const BaseSyncTarget = require('./BaseSyncTarget.js'); -const { FileApi } = require('./file-api.js'); +import JoplinServerApi from './JoplinServerApi'; +import BaseSyncTarget from './BaseSyncTarget'; +import { FileApi } from './file-api'; interface FileApiOptions { path(): string; @@ -16,27 +15,31 @@ interface FileApiOptions { export default class SyncTargetJoplinServer extends BaseSyncTarget { - static id() { + public static id() { return 9; } - static supportsConfigCheck() { + public static supportsConfigCheck() { return true; } - static targetName() { + public static targetName() { return 'joplinServer'; } - static label() { + public static label() { return _('Joplin Server'); } - async isAuthenticated() { + public async isAuthenticated() { return true; } - static async newFileApi_(options: FileApiOptions) { + public async fileApi(): Promise { + return super.fileApi(); + } + + private static async newFileApi_(options: FileApiOptions) { const apiOptions = { baseUrl: () => options.path(), username: () => options.username(), @@ -51,7 +54,7 @@ export default class SyncTargetJoplinServer extends BaseSyncTarget { return fileApi; } - static async checkConfig(options: FileApiOptions) { + public static async checkConfig(options: FileApiOptions) { const output = { ok: false, errorMessage: '', @@ -72,7 +75,7 @@ export default class SyncTargetJoplinServer extends BaseSyncTarget { return output; } - async initFileApi() { + protected async initFileApi() { const fileApi = await SyncTargetJoplinServer.newFileApi_({ path: () => Setting.value('sync.9.path'), username: () => Setting.value('sync.9.username'), @@ -85,7 +88,7 @@ export default class SyncTargetJoplinServer extends BaseSyncTarget { return fileApi; } - async initSynchronizer() { + protected async initSynchronizer() { return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType')); } } diff --git a/packages/lib/SyncTargetMemory.js b/packages/lib/SyncTargetMemory.js index 91e268c67f..5f98ff2db5 100644 --- a/packages/lib/SyncTargetMemory.js +++ b/packages/lib/SyncTargetMemory.js @@ -1,4 +1,4 @@ -const BaseSyncTarget = require('./BaseSyncTarget.js'); +const BaseSyncTarget = require('./BaseSyncTarget').default; const Setting = require('./models/Setting').default; const { FileApi } = require('./file-api.js'); const { FileApiDriverMemory } = require('./file-api-driver-memory.js'); diff --git a/packages/lib/SyncTargetNextcloud.js b/packages/lib/SyncTargetNextcloud.js index 48e2f7a340..bada7c2af4 100644 --- a/packages/lib/SyncTargetNextcloud.js +++ b/packages/lib/SyncTargetNextcloud.js @@ -1,12 +1,11 @@ // The Nextcloud sync target is essentially a wrapper over the WebDAV sync target, // thus all the calls to SyncTargetWebDAV to avoid duplicate code. -const BaseSyncTarget = require('./BaseSyncTarget.js'); +const BaseSyncTarget = require('./BaseSyncTarget').default; const { _ } = require('./locale'); const Setting = require('./models/Setting').default; const Synchronizer = require('./Synchronizer').default; const SyncTargetWebDAV = require('./SyncTargetWebDAV'); -const JoplinServerApi = require('./JoplinServerApi.js').default; class SyncTargetNextcloud extends BaseSyncTarget { @@ -50,24 +49,6 @@ class SyncTargetNextcloud extends BaseSyncTarget { return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType')); } - async appApi(settings = null) { - const useCache = !settings; - - if (this.appApi_ && useCache) return this.appApi_; - - const appApi = new JoplinServerApi({ - baseUrl: () => JoplinServerApi.baseUrlFromNextcloudWebDavUrl(settings ? settings['sync.5.path'] : Setting.value('sync.5.path')), - username: () => settings ? settings['sync.5.username'] : Setting.value('sync.5.username'), - password: () => settings ? settings['sync.5.password'] : Setting.value('sync.5.password'), - }); - - appApi.setLogger(this.logger()); - - if (useCache) this.appApi_ = appApi; - - return appApi; - } - } module.exports = SyncTargetNextcloud; diff --git a/packages/lib/SyncTargetOneDrive.ts b/packages/lib/SyncTargetOneDrive.ts index 45d8dec03e..51bd7c6c5c 100644 --- a/packages/lib/SyncTargetOneDrive.ts +++ b/packages/lib/SyncTargetOneDrive.ts @@ -2,14 +2,16 @@ import OneDriveApi from './onedrive-api'; import { _ } from './locale'; import Setting from './models/Setting'; import Synchronizer from './Synchronizer'; +import BaseSyncTarget from './BaseSyncTarget'; -const BaseSyncTarget = require('./BaseSyncTarget.js'); const { parameters } = require('./parameters.js'); const { FileApi } = require('./file-api.js'); const { FileApiDriverOneDrive } = require('./file-api-driver-onedrive.js'); export default class SyncTargetOneDrive extends BaseSyncTarget { + private api_: any; + static id() { return 3; } diff --git a/packages/lib/SyncTargetWebDAV.js b/packages/lib/SyncTargetWebDAV.js index 4d00499c73..b7879acd37 100644 --- a/packages/lib/SyncTargetWebDAV.js +++ b/packages/lib/SyncTargetWebDAV.js @@ -1,4 +1,4 @@ -const BaseSyncTarget = require('./BaseSyncTarget.js'); +const BaseSyncTarget = require('./BaseSyncTarget').default; const { _ } = require('./locale'); const Setting = require('./models/Setting').default; const { FileApi } = require('./file-api.js'); diff --git a/packages/lib/Synchronizer.ts b/packages/lib/Synchronizer.ts index 60629edbad..5fc966cfa2 100644 --- a/packages/lib/Synchronizer.ts +++ b/packages/lib/Synchronizer.ts @@ -16,6 +16,9 @@ import MasterKey from './models/MasterKey'; import BaseModel from './BaseModel'; const { sprintf } = require('sprintf-js'); import time from './time'; +import ResourceService from './services/ResourceService'; +import EncryptionService from './services/EncryptionService'; +import NoteResource from './models/NoteResource'; const JoplinError = require('./JoplinError'); const TaskQueue = require('./TaskQueue'); const { Dirnames } = require('./services/synchronizer/utils/types'); @@ -39,7 +42,8 @@ export default class Synchronizer { private clientId_: string; private lockHandler_: LockHandler; private migrationHandler_: MigrationHandler; - private encryptionService_: any = null; + private encryptionService_: EncryptionService = null; + private resourceService_: ResourceService = null; private syncTargetIsLocked_: boolean = false; // Debug flags are used to test certain hard-to-test conditions @@ -104,7 +108,7 @@ export default class Synchronizer { return this.appType_ === 'mobile' ? 100 * 1000 * 1000 : Infinity; } - setEncryptionService(v: any) { + public setEncryptionService(v: any) { this.encryptionService_ = v; } @@ -112,6 +116,14 @@ export default class Synchronizer { return this.encryptionService_; } + public setResourceService(v: ResourceService) { + this.resourceService_ = v; + } + + protected resourceService(): ResourceService { + return this.resourceService_; + } + async waitForSyncToFinish() { if (this.state() === 'idle') return; @@ -220,7 +232,7 @@ export default class Synchronizer { const iid = shim.setInterval(() => { if (this.state() == 'idle') { shim.clearInterval(iid); - resolve(); + resolve(null); } }, 100); }); @@ -332,6 +344,19 @@ export default class Synchronizer { return `${Dirnames.Resources}/${resourceId}`; }; + // We index resources and apply the "is_shared" flag before syncing + // because it's going to affect what's sent encrypted, and what's sent + // plain text. + try { + if (this.resourceService()) { + this.logger().info('Indexing resources...'); + await this.resourceService().indexNoteResources(); + await NoteResource.applySharedStatusToLinkedResources(); + } + } catch (error) { + this.logger().error('Error indexing resources:', error); + } + let errorToThrow = null; let syncLock = null; diff --git a/packages/lib/commands/synchronize.ts b/packages/lib/commands/synchronize.ts index b86e04a1a3..4509761d7f 100644 --- a/packages/lib/commands/synchronize.ts +++ b/packages/lib/commands/synchronize.ts @@ -1,6 +1,6 @@ import { utils, CommandRuntime, CommandDeclaration, CommandContext } from '../services/CommandService'; import { _ } from '../locale'; -const { reg } = require('../registry.js'); +import { reg } from '../registry'; export const declaration: CommandDeclaration = { name: 'synchronize', @@ -43,7 +43,7 @@ export const runtime = (): CommandRuntime => { sync.cancel(); return 'cancel'; } else { - reg.scheduleSync(0); + void reg.scheduleSync(0); return 'sync'; } }, diff --git a/packages/lib/components/shared/config-shared.js b/packages/lib/components/shared/config-shared.js index b8af57e0b9..9dbc0773e7 100644 --- a/packages/lib/components/shared/config-shared.js +++ b/packages/lib/components/shared/config-shared.js @@ -3,7 +3,6 @@ const SyncTargetRegistry = require('../../SyncTargetRegistry'); const ObjectUtils = require('../../ObjectUtils'); const { _ } = require('../../locale'); const { createSelector } = require('reselect'); -const { reg } = require('../../registry'); const shared = {}; @@ -32,7 +31,7 @@ shared.checkSyncConfig = async function(comp, settings) { comp.setState({ checkSyncConfigResult: result }); if (result.ok) { - await shared.checkNextcloudApp(comp, settings); + // await shared.checkNextcloudApp(comp, settings); // Users often expect config to be auto-saved at this point, if the config check was successful shared.saveSettings(comp); } @@ -54,30 +53,6 @@ shared.checkSyncConfigMessages = function(comp) { return output; }; -shared.checkNextcloudApp = async function(comp, settings) { - if (settings['sync.target'] !== 5) return; - - comp.setState({ checkNextcloudAppResult: 'checking' }); - let result = null; - const appApi = await reg.syncTargetNextcloud().appApi(settings); - - try { - result = await appApi.setupSyncTarget(settings['sync.5.path']); - } catch (error) { - reg.logger().error('Could not setup sync target:', error); - result = { error: error.message }; - } - - const newSyncTargets = Object.assign({}, settings['sync.5.syncTargets']); - newSyncTargets[settings['sync.5.path']] = result; - shared.updateSettingValue(comp, 'sync.5.syncTargets', newSyncTargets); - - // Also immediately save the result as this is most likely what the user would expect - Setting.setValue('sync.5.syncTargets', newSyncTargets); - - comp.setState({ checkNextcloudAppResult: 'done' }); -}; - shared.updateSettingValue = function(comp, key, value) { comp.setState(state => { const settings = Object.assign({}, state.settings); diff --git a/packages/lib/database.js b/packages/lib/database.js index f15e9cfc64..99d1a60a3e 100644 --- a/packages/lib/database.js +++ b/packages/lib/database.js @@ -1,321 +1,358 @@ -const Logger = require('./Logger').default; -const time = require('./time').default; +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const Logger_1 = require("./Logger"); +const time_1 = require("./time"); +const shim_1 = require("./shim"); const Mutex = require('async-mutex').Mutex; -const shim = require('./shim').default; - class Database { - constructor(driver) { - this.debugMode_ = false; - this.sqlQueryLogEnabled_ = false; - this.driver_ = driver; - this.logger_ = new Logger(); - this.logExcludedQueryTypes_ = []; - this.batchTransactionMutex_ = new Mutex(); - this.profilingEnabled_ = false; - this.queryId_ = 1; - } - - setLogExcludedQueryTypes(v) { - this.logExcludedQueryTypes_ = v; - } - - // Converts the SQLite error to a regular JS error - // so that it prints a stacktrace when passed to - // console.error() - sqliteErrorToJsError(error, sql = null, params = null) { - return this.driver().sqliteErrorToJsError(error, sql, params); - } - - setLogger(l) { - this.logger_ = l; - } - - logger() { - return this.logger_; - } - - driver() { - return this.driver_; - } - - async open(options) { - try { - await this.driver().open(options); - } catch (error) { - throw new Error(`Cannot open database: ${error.message}: ${JSON.stringify(options)}`); - } - - this.logger().info('Database was open successfully'); - } - - escapeField(field) { - if (field == '*') return '*'; - const p = field.split('.'); - if (p.length == 1) return `\`${field}\``; - if (p.length == 2) return `${p[0]}.\`${p[1]}\``; - - throw new Error(`Invalid field format: ${field}`); - } - - escapeFields(fields) { - if (fields == '*') return '*'; - - const output = []; - for (let i = 0; i < fields.length; i++) { - output.push(this.escapeField(fields[i])); - } - return output; - } - - async tryCall(callName, sql, params) { - if (typeof sql === 'object') { - params = sql.params; - sql = sql.sql; - } - - let waitTime = 50; - let totalWaitTime = 0; - const callStartTime = Date.now(); - let profilingTimeoutId = null; - while (true) { - try { - this.logQuery(sql, params); - - const queryId = this.queryId_++; - if (this.profilingEnabled_) { - console.info(`SQL START ${queryId}`, sql, params); - - profilingTimeoutId = shim.setInterval(() => { - console.warn(`SQL ${queryId} has been running for ${Date.now() - callStartTime}: ${sql}`); - }, 3000); - } - - const result = await this.driver()[callName](sql, params); - - if (this.profilingEnabled_) { - shim.clearInterval(profilingTimeoutId); - profilingTimeoutId = null; - const elapsed = Date.now() - callStartTime; - if (elapsed > 10) console.info(`SQL END ${queryId}`, elapsed, sql, params); - } - - return result; // No exception was thrown - } catch (error) { - if (error && (error.code == 'SQLITE_IOERR' || error.code == 'SQLITE_BUSY')) { - if (totalWaitTime >= 20000) throw this.sqliteErrorToJsError(error, sql, params); - // NOTE: don't put logger statements here because it might log to the database, which - // could result in an error being thrown again. - // this.logger().warn(sprintf('Error %s: will retry in %s milliseconds', error.code, waitTime)); - // this.logger().warn('Error was: ' + error.toString()); - await time.msleep(waitTime); - totalWaitTime += waitTime; - waitTime *= 1.5; - } else { - throw this.sqliteErrorToJsError(error, sql, params); - } - } finally { - if (profilingTimeoutId) shim.clearInterval(profilingTimeoutId); - } - } - } - - async selectOne(sql, params = null) { - return this.tryCall('selectOne', sql, params); - } - - async loadExtension(/* path */) { - return; // Disabled for now as fuzzy search extension is not in use - - // let result = null; - // try { - // result = await this.driver().loadExtension(path); - // return result; - // } catch (e) { - // throw new Error(`Could not load extension ${path}`); - // } - } - - async selectAll(sql, params = null) { - return this.tryCall('selectAll', sql, params); - } - - async selectAllFields(sql, params, field) { - const rows = await this.tryCall('selectAll', sql, params); - const output = []; - for (let i = 0; i < rows.length; i++) { - const v = rows[i][field]; - if (!v) throw new Error(`No such field: ${field}. Query was: ${sql}`); - output.push(rows[i][field]); - } - return output; - } - - async exec(sql, params = null) { - return this.tryCall('exec', sql, params); - } - - async transactionExecBatch(queries) { - if (queries.length <= 0) return; - - if (queries.length == 1) { - const q = this.wrapQuery(queries[0]); - await this.exec(q.sql, q.params); - return; - } - - // There can be only one transaction running at a time so use a mutex - const release = await this.batchTransactionMutex_.acquire(); - - try { - await this.exec('BEGIN TRANSACTION'); - - for (let i = 0; i < queries.length; i++) { - const query = this.wrapQuery(queries[i]); - await this.exec(query.sql, query.params); - } - - await this.exec('COMMIT'); - } catch (error) { - await this.exec('ROLLBACK'); - throw error; - } finally { - release(); - } - } - - static enumId(type, s) { - if (type == 'settings') { - if (s == 'int') return 1; - if (s == 'string') return 2; - } - if (type == 'fieldType') { - if (s) s = s.toUpperCase(); - if (s == 'INTEGER') s = 'INT'; - if (!(`TYPE_${s}` in this)) throw new Error(`Unkonwn fieldType: ${s}`); - return this[`TYPE_${s}`]; - } - if (type == 'syncTarget') { - if (s == 'memory') return 1; - if (s == 'filesystem') return 2; - if (s == 'onedrive') return 3; - } - throw new Error(`Unknown enum type or value: ${type}, ${s}`); - } - - static enumName(type, id) { - if (type === 'fieldType') { - if (id === Database.TYPE_UNKNOWN) return 'unknown'; - if (id === Database.TYPE_INT) return 'int'; - if (id === Database.TYPE_TEXT) return 'text'; - if (id === Database.TYPE_NUMERIC) return 'numeric'; - throw new Error(`Invalid type id: ${id}`); - } - } - - static formatValue(type, value) { - if (value === null || value === undefined) return null; - if (type == this.TYPE_INT) return Number(value); - if (type == this.TYPE_TEXT) return value; - if (type == this.TYPE_NUMERIC) return Number(value); - throw new Error(`Unknown type: ${type}`); - } - - sqlStringToLines(sql) { - const output = []; - const lines = sql.split('\n'); - let statement = ''; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line == '') continue; - if (line.substr(0, 2) == '--') continue; - statement += line.trim(); - if (line[line.length - 1] == ',') statement += ' '; - if (line[line.length - 1] == ';') { - output.push(statement); - statement = ''; - } - } - return output; - } - - logQuery(sql, params = null) { - if (!this.sqlQueryLogEnabled_) return; - - if (this.logExcludedQueryTypes_.length) { - const temp = sql.toLowerCase(); - for (let i = 0; i < this.logExcludedQueryTypes_.length; i++) { - if (temp.indexOf(this.logExcludedQueryTypes_[i].toLowerCase()) === 0) return; - } - } - - this.logger().debug(sql); - if (params !== null && params.length) this.logger().debug(JSON.stringify(params)); - } - - static insertQuery(tableName, data) { - if (!data || !Object.keys(data).length) throw new Error('Data is empty'); - - let keySql = ''; - let valueSql = ''; - const params = []; - for (const key in data) { - if (!data.hasOwnProperty(key)) continue; - if (key[key.length - 1] == '_') continue; - if (keySql != '') keySql += ', '; - if (valueSql != '') valueSql += ', '; - keySql += `\`${key}\``; - valueSql += '?'; - params.push(data[key]); - } - return { - sql: `INSERT INTO \`${tableName}\` (${keySql}) VALUES (${valueSql})`, - params: params, - }; - } - - static updateQuery(tableName, data, where) { - if (!data || !Object.keys(data).length) throw new Error('Data is empty'); - - let sql = ''; - const params = []; - for (const key in data) { - if (!data.hasOwnProperty(key)) continue; - if (key[key.length - 1] == '_') continue; - if (sql != '') sql += ', '; - sql += `\`${key}\`=?`; - params.push(data[key]); - } - - if (typeof where != 'string') { - const s = []; - for (const n in where) { - if (!where.hasOwnProperty(n)) continue; - params.push(where[n]); - s.push(`\`${n}\`=?`); - } - where = s.join(' AND '); - } - - return { - sql: `UPDATE \`${tableName}\` SET ${sql} WHERE ${where}`, - params: params, - }; - } - - alterColumnQueries(tableName, fields) { - const fieldsNoType = []; - for (const n in fields) { - if (!fields.hasOwnProperty(n)) continue; - fieldsNoType.push(n); - } - - const fieldsWithType = []; - for (const n in fields) { - if (!fields.hasOwnProperty(n)) continue; - fieldsWithType.push(`${this.escapeField(n)} ${fields[n]}`); - } - - let sql = ` + constructor(driver) { + this.debugMode_ = false; + this.sqlQueryLogEnabled_ = false; + this.logger_ = new Logger_1.default(); + this.logExcludedQueryTypes_ = []; + this.batchTransactionMutex_ = new Mutex(); + this.profilingEnabled_ = false; + this.queryId_ = 1; + this.driver_ = driver; + } + setLogExcludedQueryTypes(v) { + this.logExcludedQueryTypes_ = v; + } + // Converts the SQLite error to a regular JS error + // so that it prints a stacktrace when passed to + // console.error() + sqliteErrorToJsError(error, sql = null, params = null) { + return this.driver().sqliteErrorToJsError(error, sql, params); + } + setLogger(l) { + this.logger_ = l; + } + logger() { + return this.logger_; + } + driver() { + return this.driver_; + } + open(options) { + return __awaiter(this, void 0, void 0, function* () { + try { + yield this.driver().open(options); + } + catch (error) { + throw new Error(`Cannot open database: ${error.message}: ${JSON.stringify(options)}`); + } + this.logger().info('Database was open successfully'); + }); + } + escapeField(field) { + if (field == '*') + return '*'; + const p = field.split('.'); + if (p.length == 1) + return `\`${field}\``; + if (p.length == 2) + return `${p[0]}.\`${p[1]}\``; + throw new Error(`Invalid field format: ${field}`); + } + escapeFields(fields) { + if (fields == '*') + return '*'; + const output = []; + for (let i = 0; i < fields.length; i++) { + output.push(this.escapeField(fields[i])); + } + return output; + } + tryCall(callName, inputSql, inputParams) { + return __awaiter(this, void 0, void 0, function* () { + let sql = null; + let params = null; + if (typeof inputSql === 'object') { + params = inputSql.params; + sql = inputSql.sql; + } + else { + params = inputParams; + sql = inputSql; + } + let waitTime = 50; + let totalWaitTime = 0; + const callStartTime = Date.now(); + let profilingTimeoutId = null; + while (true) { + try { + this.logQuery(sql, params); + const queryId = this.queryId_++; + if (this.profilingEnabled_) { + console.info(`SQL START ${queryId}`, sql, params); + profilingTimeoutId = shim_1.default.setInterval(() => { + console.warn(`SQL ${queryId} has been running for ${Date.now() - callStartTime}: ${sql}`); + }, 3000); + } + const result = yield this.driver()[callName](sql, params); + if (this.profilingEnabled_) { + shim_1.default.clearInterval(profilingTimeoutId); + profilingTimeoutId = null; + const elapsed = Date.now() - callStartTime; + if (elapsed > 10) + console.info(`SQL END ${queryId}`, elapsed, sql, params); + } + return result; // No exception was thrown + } + catch (error) { + if (error && (error.code == 'SQLITE_IOERR' || error.code == 'SQLITE_BUSY')) { + if (totalWaitTime >= 20000) + throw this.sqliteErrorToJsError(error, sql, params); + // NOTE: don't put logger statements here because it might log to the database, which + // could result in an error being thrown again. + // this.logger().warn(sprintf('Error %s: will retry in %s milliseconds', error.code, waitTime)); + // this.logger().warn('Error was: ' + error.toString()); + yield time_1.default.msleep(waitTime); + totalWaitTime += waitTime; + waitTime *= 1.5; + } + else { + throw this.sqliteErrorToJsError(error, sql, params); + } + } + finally { + if (profilingTimeoutId) + shim_1.default.clearInterval(profilingTimeoutId); + } + } + }); + } + selectOne(sql, params = null) { + return __awaiter(this, void 0, void 0, function* () { + return this.tryCall('selectOne', sql, params); + }); + } + loadExtension( /* path */) { + return __awaiter(this, void 0, void 0, function* () { + return; // Disabled for now as fuzzy search extension is not in use + // let result = null; + // try { + // result = await this.driver().loadExtension(path); + // return result; + // } catch (e) { + // throw new Error(`Could not load extension ${path}`); + // } + }); + } + selectAll(sql, params = null) { + return __awaiter(this, void 0, void 0, function* () { + return this.tryCall('selectAll', sql, params); + }); + } + selectAllFields(sql, params, field) { + return __awaiter(this, void 0, void 0, function* () { + const rows = yield this.tryCall('selectAll', sql, params); + const output = []; + for (let i = 0; i < rows.length; i++) { + const v = rows[i][field]; + if (!v) + throw new Error(`No such field: ${field}. Query was: ${sql}`); + output.push(rows[i][field]); + } + return output; + }); + } + exec(sql, params = null) { + return __awaiter(this, void 0, void 0, function* () { + return this.tryCall('exec', sql, params); + }); + } + transactionExecBatch(queries) { + return __awaiter(this, void 0, void 0, function* () { + if (queries.length <= 0) + return; + if (queries.length == 1) { + const q = this.wrapQuery(queries[0]); + yield this.exec(q.sql, q.params); + return; + } + // There can be only one transaction running at a time so use a mutex + const release = yield this.batchTransactionMutex_.acquire(); + try { + yield this.exec('BEGIN TRANSACTION'); + for (let i = 0; i < queries.length; i++) { + const query = this.wrapQuery(queries[i]); + yield this.exec(query.sql, query.params); + } + yield this.exec('COMMIT'); + } + catch (error) { + yield this.exec('ROLLBACK'); + throw error; + } + finally { + release(); + } + }); + } + static enumId(type, s) { + if (type == 'settings') { + if (s == 'int') + return 1; + if (s == 'string') + return 2; + } + if (type == 'fieldType') { + if (s) + s = s.toUpperCase(); + if (s == 'INTEGER') + s = 'INT'; + if (!(`TYPE_${s}` in this)) + throw new Error(`Unkonwn fieldType: ${s}`); + return this[`TYPE_${s}`]; + } + if (type == 'syncTarget') { + if (s == 'memory') + return 1; + if (s == 'filesystem') + return 2; + if (s == 'onedrive') + return 3; + } + throw new Error(`Unknown enum type or value: ${type}, ${s}`); + } + static enumName(type, id) { + if (type === 'fieldType') { + if (id === Database.TYPE_UNKNOWN) + return 'unknown'; + if (id === Database.TYPE_INT) + return 'int'; + if (id === Database.TYPE_TEXT) + return 'text'; + if (id === Database.TYPE_NUMERIC) + return 'numeric'; + throw new Error(`Invalid type id: ${id}`); + } + // Or maybe an error should be thrown + return undefined; + } + static formatValue(type, value) { + if (value === null || value === undefined) + return null; + if (type == this.TYPE_INT) + return Number(value); + if (type == this.TYPE_TEXT) + return value; + if (type == this.TYPE_NUMERIC) + return Number(value); + throw new Error(`Unknown type: ${type}`); + } + sqlStringToLines(sql) { + const output = []; + const lines = sql.split('\n'); + let statement = ''; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line == '') + continue; + if (line.substr(0, 2) == '--') + continue; + statement += line.trim(); + if (line[line.length - 1] == ',') + statement += ' '; + if (line[line.length - 1] == ';') { + output.push(statement); + statement = ''; + } + } + return output; + } + logQuery(sql, params = null) { + if (!this.sqlQueryLogEnabled_) + return; + if (this.logExcludedQueryTypes_.length) { + const temp = sql.toLowerCase(); + for (let i = 0; i < this.logExcludedQueryTypes_.length; i++) { + if (temp.indexOf(this.logExcludedQueryTypes_[i].toLowerCase()) === 0) + return; + } + } + this.logger().debug(sql); + if (params !== null && params.length) + this.logger().debug(JSON.stringify(params)); + } + static insertQuery(tableName, data) { + if (!data || !Object.keys(data).length) + throw new Error('Data is empty'); + let keySql = ''; + let valueSql = ''; + const params = []; + for (const key in data) { + if (!data.hasOwnProperty(key)) + continue; + if (key[key.length - 1] == '_') + continue; + if (keySql != '') + keySql += ', '; + if (valueSql != '') + valueSql += ', '; + keySql += `\`${key}\``; + valueSql += '?'; + params.push(data[key]); + } + return { + sql: `INSERT INTO \`${tableName}\` (${keySql}) VALUES (${valueSql})`, + params: params, + }; + } + static updateQuery(tableName, data, where) { + if (!data || !Object.keys(data).length) + throw new Error('Data is empty'); + let sql = ''; + const params = []; + for (const key in data) { + if (!data.hasOwnProperty(key)) + continue; + if (key[key.length - 1] == '_') + continue; + if (sql != '') + sql += ', '; + sql += `\`${key}\`=?`; + params.push(data[key]); + } + if (typeof where != 'string') { + const s = []; + for (const n in where) { + if (!where.hasOwnProperty(n)) + continue; + params.push(where[n]); + s.push(`\`${n}\`=?`); + } + where = s.join(' AND '); + } + return { + sql: `UPDATE \`${tableName}\` SET ${sql} WHERE ${where}`, + params: params, + }; + } + alterColumnQueries(tableName, fields) { + const fieldsNoType = []; + for (const n in fields) { + if (!fields.hasOwnProperty(n)) + continue; + fieldsNoType.push(n); + } + const fieldsWithType = []; + for (const n in fields) { + if (!fields.hasOwnProperty(n)) + continue; + fieldsWithType.push(`${this.escapeField(n)} ${fields[n]}`); + } + let sql = ` CREATE TEMPORARY TABLE _BACKUP_TABLE_NAME_(_FIELDS_TYPE_); INSERT INTO _BACKUP_TABLE_NAME_ SELECT _FIELDS_NO_TYPE_ FROM _TABLE_NAME_; DROP TABLE _TABLE_NAME_; @@ -323,42 +360,39 @@ class Database { INSERT INTO _TABLE_NAME_ SELECT _FIELDS_NO_TYPE_ FROM _BACKUP_TABLE_NAME_; DROP TABLE _BACKUP_TABLE_NAME_; `; - - sql = sql.replace(/_BACKUP_TABLE_NAME_/g, this.escapeField(`${tableName}_backup`)); - sql = sql.replace(/_TABLE_NAME_/g, this.escapeField(tableName)); - sql = sql.replace(/_FIELDS_NO_TYPE_/g, this.escapeFields(fieldsNoType).join(',')); - sql = sql.replace(/_FIELDS_TYPE_/g, fieldsWithType.join(',')); - - return sql.trim().split('\n'); - } - - wrapQueries(queries) { - const output = []; - for (let i = 0; i < queries.length; i++) { - output.push(this.wrapQuery(queries[i])); - } - return output; - } - - wrapQuery(sql, params = null) { - if (!sql) throw new Error(`Cannot wrap empty string: ${sql}`); - - if (sql.constructor === Array) { - const output = {}; - output.sql = sql[0]; - output.params = sql.length >= 2 ? sql[1] : null; - return output; - } else if (typeof sql === 'string') { - return { sql: sql, params: params }; - } else { - return sql; // Already wrapped - } - } + sql = sql.replace(/_BACKUP_TABLE_NAME_/g, this.escapeField(`${tableName}_backup`)); + sql = sql.replace(/_TABLE_NAME_/g, this.escapeField(tableName)); + sql = sql.replace(/_FIELDS_NO_TYPE_/g, this.escapeFields(fieldsNoType).join(',')); + sql = sql.replace(/_FIELDS_TYPE_/g, fieldsWithType.join(',')); + return sql.trim().split('\n'); + } + wrapQueries(queries) { + const output = []; + for (let i = 0; i < queries.length; i++) { + output.push(this.wrapQuery(queries[i])); + } + return output; + } + wrapQuery(sql, params = null) { + if (!sql) + throw new Error(`Cannot wrap empty string: ${sql}`); + if (Array.isArray(sql)) { + return { + sql: sql[0], + params: sql.length >= 2 ? sql[1] : null, + }; + } + else if (typeof sql === 'string') { + return { sql: sql, params: params }; + } + else { + return sql; // Already wrapped + } + } } - +exports.default = Database; Database.TYPE_UNKNOWN = 0; Database.TYPE_INT = 1; Database.TYPE_TEXT = 2; Database.TYPE_NUMERIC = 3; - -module.exports = { Database }; +//# sourceMappingURL=database.js.map \ No newline at end of file diff --git a/packages/lib/database.ts b/packages/lib/database.ts new file mode 100644 index 0000000000..9a03ee6de3 --- /dev/null +++ b/packages/lib/database.ts @@ -0,0 +1,386 @@ +import Logger from './Logger'; +import time from './time'; +import shim from './shim'; + +const Mutex = require('async-mutex').Mutex; + +type SqlParams = Record; + +export interface SqlQuery { + sql: string; + params?: SqlParams; +} + +type StringOrSqlQuery = string | SqlQuery; + +export type Row = Record; + +export default class Database { + + public static TYPE_UNKNOWN = 0; + public static TYPE_INT = 1; + public static TYPE_TEXT = 2; + public static TYPE_NUMERIC = 3; + + protected debugMode_ = false; + private sqlQueryLogEnabled_ = false; + private driver_: any; + private logger_ = new Logger(); + private logExcludedQueryTypes_: string[] = []; + private batchTransactionMutex_ = new Mutex(); + private profilingEnabled_ = false; + private queryId_ = 1; + + public constructor(driver: any) { + this.driver_ = driver; + } + + setLogExcludedQueryTypes(v: string[]) { + this.logExcludedQueryTypes_ = v; + } + + // Converts the SQLite error to a regular JS error + // so that it prints a stacktrace when passed to + // console.error() + sqliteErrorToJsError(error: any, sql: string = null, params: SqlParams = null) { + return this.driver().sqliteErrorToJsError(error, sql, params); + } + + setLogger(l: Logger) { + this.logger_ = l; + } + + logger() { + return this.logger_; + } + + driver() { + return this.driver_; + } + + async open(options: any) { + try { + await this.driver().open(options); + } catch (error) { + throw new Error(`Cannot open database: ${error.message}: ${JSON.stringify(options)}`); + } + + this.logger().info('Database was open successfully'); + } + + escapeField(field: string) { + if (field == '*') return '*'; + const p = field.split('.'); + if (p.length == 1) return `\`${field}\``; + if (p.length == 2) return `${p[0]}.\`${p[1]}\``; + + throw new Error(`Invalid field format: ${field}`); + } + + escapeFields(fields: string[] | string): string[] | string { + if (fields == '*') return '*'; + + const output = []; + for (let i = 0; i < fields.length; i++) { + output.push(this.escapeField(fields[i])); + } + return output; + } + + async tryCall(callName: string, inputSql: StringOrSqlQuery, inputParams: SqlParams) { + let sql: string = null; + let params: SqlParams = null; + + if (typeof inputSql === 'object') { + params = (inputSql as SqlQuery).params; + sql = (inputSql as SqlQuery).sql; + } else { + params = inputParams; + sql = inputSql as string; + } + + let waitTime = 50; + let totalWaitTime = 0; + const callStartTime = Date.now(); + let profilingTimeoutId = null; + while (true) { + try { + this.logQuery(sql, params); + + const queryId = this.queryId_++; + if (this.profilingEnabled_) { + console.info(`SQL START ${queryId}`, sql, params); + + profilingTimeoutId = shim.setInterval(() => { + console.warn(`SQL ${queryId} has been running for ${Date.now() - callStartTime}: ${sql}`); + }, 3000); + } + + const result = await this.driver()[callName](sql, params); + + if (this.profilingEnabled_) { + shim.clearInterval(profilingTimeoutId); + profilingTimeoutId = null; + const elapsed = Date.now() - callStartTime; + if (elapsed > 10) console.info(`SQL END ${queryId}`, elapsed, sql, params); + } + + return result; // No exception was thrown + } catch (error) { + if (error && (error.code == 'SQLITE_IOERR' || error.code == 'SQLITE_BUSY')) { + if (totalWaitTime >= 20000) throw this.sqliteErrorToJsError(error, sql, params); + // NOTE: don't put logger statements here because it might log to the database, which + // could result in an error being thrown again. + // this.logger().warn(sprintf('Error %s: will retry in %s milliseconds', error.code, waitTime)); + // this.logger().warn('Error was: ' + error.toString()); + await time.msleep(waitTime); + totalWaitTime += waitTime; + waitTime *= 1.5; + } else { + throw this.sqliteErrorToJsError(error, sql, params); + } + } finally { + if (profilingTimeoutId) shim.clearInterval(profilingTimeoutId); + } + } + } + + async selectOne(sql: string, params: SqlParams = null): Promise { + return this.tryCall('selectOne', sql, params); + } + + async loadExtension(/* path */) { + return; // Disabled for now as fuzzy search extension is not in use + + // let result = null; + // try { + // result = await this.driver().loadExtension(path); + // return result; + // } catch (e) { + // throw new Error(`Could not load extension ${path}`); + // } + } + + async selectAll(sql: string, params: SqlParams = null): Promise { + return this.tryCall('selectAll', sql, params); + } + + async selectAllFields(sql: string, params: SqlParams, field: string): Promise { + const rows = await this.tryCall('selectAll', sql, params); + const output = []; + for (let i = 0; i < rows.length; i++) { + const v = rows[i][field]; + if (!v) throw new Error(`No such field: ${field}. Query was: ${sql}`); + output.push(rows[i][field]); + } + return output; + } + + async exec(sql: StringOrSqlQuery, params: SqlParams = null) { + return this.tryCall('exec', sql, params); + } + + async transactionExecBatch(queries: StringOrSqlQuery[]) { + if (queries.length <= 0) return; + + if (queries.length == 1) { + const q = this.wrapQuery(queries[0]); + await this.exec(q.sql, q.params); + return; + } + + // There can be only one transaction running at a time so use a mutex + const release = await this.batchTransactionMutex_.acquire(); + + try { + await this.exec('BEGIN TRANSACTION'); + + for (let i = 0; i < queries.length; i++) { + const query = this.wrapQuery(queries[i]); + await this.exec(query.sql, query.params); + } + + await this.exec('COMMIT'); + } catch (error) { + await this.exec('ROLLBACK'); + throw error; + } finally { + release(); + } + } + + static enumId(type: string, s: string) { + if (type == 'settings') { + if (s == 'int') return 1; + if (s == 'string') return 2; + } + if (type == 'fieldType') { + if (s) s = s.toUpperCase(); + if (s == 'INTEGER') s = 'INT'; + if (!(`TYPE_${s}` in this)) throw new Error(`Unkonwn fieldType: ${s}`); + return (this as any)[`TYPE_${s}`]; + } + if (type == 'syncTarget') { + if (s == 'memory') return 1; + if (s == 'filesystem') return 2; + if (s == 'onedrive') return 3; + } + throw new Error(`Unknown enum type or value: ${type}, ${s}`); + } + + static enumName(type: string, id: number) { + if (type === 'fieldType') { + if (id === Database.TYPE_UNKNOWN) return 'unknown'; + if (id === Database.TYPE_INT) return 'int'; + if (id === Database.TYPE_TEXT) return 'text'; + if (id === Database.TYPE_NUMERIC) return 'numeric'; + throw new Error(`Invalid type id: ${id}`); + } + + // Or maybe an error should be thrown + return undefined; + } + + static formatValue(type: number, value: any) { + if (value === null || value === undefined) return null; + if (type == this.TYPE_INT) return Number(value); + if (type == this.TYPE_TEXT) return value; + if (type == this.TYPE_NUMERIC) return Number(value); + throw new Error(`Unknown type: ${type}`); + } + + sqlStringToLines(sql: string) { + const output = []; + const lines = sql.split('\n'); + let statement = ''; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line == '') continue; + if (line.substr(0, 2) == '--') continue; + statement += line.trim(); + if (line[line.length - 1] == ',') statement += ' '; + if (line[line.length - 1] == ';') { + output.push(statement); + statement = ''; + } + } + return output; + } + + logQuery(sql: string, params: SqlParams = null) { + if (!this.sqlQueryLogEnabled_) return; + + if (this.logExcludedQueryTypes_.length) { + const temp = sql.toLowerCase(); + for (let i = 0; i < this.logExcludedQueryTypes_.length; i++) { + if (temp.indexOf(this.logExcludedQueryTypes_[i].toLowerCase()) === 0) return; + } + } + + this.logger().debug(sql); + if (params !== null && params.length) this.logger().debug(JSON.stringify(params)); + } + + static insertQuery(tableName: string, data: Record) { + if (!data || !Object.keys(data).length) throw new Error('Data is empty'); + + let keySql = ''; + let valueSql = ''; + const params = []; + for (const key in data) { + if (!data.hasOwnProperty(key)) continue; + if (key[key.length - 1] == '_') continue; + if (keySql != '') keySql += ', '; + if (valueSql != '') valueSql += ', '; + keySql += `\`${key}\``; + valueSql += '?'; + params.push(data[key]); + } + return { + sql: `INSERT INTO \`${tableName}\` (${keySql}) VALUES (${valueSql})`, + params: params, + }; + } + + static updateQuery(tableName: string, data: Record, where: string | Record) { + if (!data || !Object.keys(data).length) throw new Error('Data is empty'); + + let sql = ''; + const params = []; + for (const key in data) { + if (!data.hasOwnProperty(key)) continue; + if (key[key.length - 1] == '_') continue; + if (sql != '') sql += ', '; + sql += `\`${key}\`=?`; + params.push(data[key]); + } + + if (typeof where != 'string') { + const s = []; + for (const n in where) { + if (!where.hasOwnProperty(n)) continue; + params.push(where[n]); + s.push(`\`${n}\`=?`); + } + where = s.join(' AND '); + } + + return { + sql: `UPDATE \`${tableName}\` SET ${sql} WHERE ${where}`, + params: params, + }; + } + + alterColumnQueries(tableName: string, fields: Record) { + const fieldsNoType = []; + for (const n in fields) { + if (!fields.hasOwnProperty(n)) continue; + fieldsNoType.push(n); + } + + const fieldsWithType = []; + for (const n in fields) { + if (!fields.hasOwnProperty(n)) continue; + fieldsWithType.push(`${this.escapeField(n)} ${fields[n]}`); + } + + let sql = ` + CREATE TEMPORARY TABLE _BACKUP_TABLE_NAME_(_FIELDS_TYPE_); + INSERT INTO _BACKUP_TABLE_NAME_ SELECT _FIELDS_NO_TYPE_ FROM _TABLE_NAME_; + DROP TABLE _TABLE_NAME_; + CREATE TABLE _TABLE_NAME_(_FIELDS_TYPE_); + INSERT INTO _TABLE_NAME_ SELECT _FIELDS_NO_TYPE_ FROM _BACKUP_TABLE_NAME_; + DROP TABLE _BACKUP_TABLE_NAME_; + `; + + sql = sql.replace(/_BACKUP_TABLE_NAME_/g, this.escapeField(`${tableName}_backup`)); + sql = sql.replace(/_TABLE_NAME_/g, this.escapeField(tableName)); + sql = sql.replace(/_FIELDS_NO_TYPE_/g, (this.escapeFields(fieldsNoType) as string[]).join(',')); + sql = sql.replace(/_FIELDS_TYPE_/g, fieldsWithType.join(',')); + + return sql.trim().split('\n'); + } + + wrapQueries(queries: any[]) { + const output = []; + for (let i = 0; i < queries.length; i++) { + output.push(this.wrapQuery(queries[i])); + } + return output; + } + + wrapQuery(sql: any, params: SqlParams = null): SqlQuery { + if (!sql) throw new Error(`Cannot wrap empty string: ${sql}`); + + if (Array.isArray(sql)) { + return { + sql: sql[0], + params: sql.length >= 2 ? sql[1] : null, + }; + } else if (typeof sql === 'string') { + return { sql: sql, params: params }; + } else { + return sql; // Already wrapped + } + } +} diff --git a/packages/lib/file-api-driver-joplinServer.ts b/packages/lib/file-api-driver-joplinServer.ts index 69b393f626..6d0af029e3 100644 --- a/packages/lib/file-api-driver-joplinServer.ts +++ b/packages/lib/file-api-driver-joplinServer.ts @@ -1,4 +1,4 @@ -import JoplinServerApi from './JoplinServerApi2'; +import JoplinServerApi from './JoplinServerApi'; const { dirname, basename } = require('./path-utils'); function removeTrailingColon(path: string) { diff --git a/packages/lib/file-api-driver-onedrive.js b/packages/lib/file-api-driver-onedrive.js index a9c07310e1..e016f174b7 100644 --- a/packages/lib/file-api-driver-onedrive.js +++ b/packages/lib/file-api-driver-onedrive.js @@ -198,7 +198,7 @@ class FileApiDriverOneDrive { async clearRoot() { const recurseItems = async (path) => { - const result = await this.list(this.fileApi_.fullPath_(path)); + const result = await this.list(this.fileApi_.fullPath(path)); const output = []; for (const item of result.items) { @@ -206,7 +206,7 @@ class FileApiDriverOneDrive { if (item.isDir) { await recurseItems(fullPath); } - await this.delete(this.fileApi_.fullPath_(fullPath)); + await this.delete(this.fileApi_.fullPath(fullPath)); } return output; diff --git a/packages/lib/file-api.js b/packages/lib/file-api.js index 0a6eada8be..46e57c9c4b 100644 --- a/packages/lib/file-api.js +++ b/packages/lib/file-api.js @@ -1,452 +1,429 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.basicDelta = exports.FileApi = void 0; +const Logger_1 = require("./Logger"); +const shim_1 = require("./shim"); +const BaseItem_1 = require("./models/BaseItem"); +const time_1 = require("./time"); const { isHidden } = require('./path-utils'); -const Logger = require('./Logger').default; -const shim = require('./shim').default; -const BaseItem = require('./models/BaseItem').default; const JoplinError = require('./JoplinError'); const ArrayUtils = require('./ArrayUtils'); -const time = require('./time').default; const { sprintf } = require('sprintf-js'); const Mutex = require('async-mutex').Mutex; - -const logger = Logger.create('FileApi'); - +const logger = Logger_1.default.create('FileApi'); function requestCanBeRepeated(error) { - const errorCode = typeof error === 'object' && error.code ? error.code : null; - - // The target is explicitely rejecting the item so repeating wouldn't make a difference. - if (errorCode === 'rejectedByTarget') return false; - - // We don't repeat failSafe errors because it's an indication of an issue at the - // server-level issue which usually cannot be fixed by repeating the request. - // Also we print the previous requests and responses to the log in this case, - // so not repeating means there will be less noise in the log. - if (errorCode === 'failSafe') return false; - - return true; + const errorCode = typeof error === 'object' && error.code ? error.code : null; + // The target is explicitely rejecting the item so repeating wouldn't make a difference. + if (errorCode === 'rejectedByTarget') + return false; + // We don't repeat failSafe errors because it's an indication of an issue at the + // server-level issue which usually cannot be fixed by repeating the request. + // Also we print the previous requests and responses to the log in this case, + // so not repeating means there will be less noise in the log. + if (errorCode === 'failSafe') + return false; + return true; } - -async function tryAndRepeat(fn, count) { - let retryCount = 0; - - // Don't use internal fetch retry mechanim since we - // are already retrying here. - const shimFetchMaxRetryPrevious = shim.fetchMaxRetrySet(0); - const defer = () => { - shim.fetchMaxRetrySet(shimFetchMaxRetryPrevious); - }; - - while (true) { - try { - const result = await fn(); - defer(); - return result; - } catch (error) { - if (retryCount >= count || !requestCanBeRepeated(error)) { - defer(); - throw error; - } - retryCount++; - await time.sleep(1 + retryCount * 3); - } - } +function tryAndRepeat(fn, count) { + return __awaiter(this, void 0, void 0, function* () { + let retryCount = 0; + // Don't use internal fetch retry mechanim since we + // are already retrying here. + const shimFetchMaxRetryPrevious = shim_1.default.fetchMaxRetrySet(0); + const defer = () => { + shim_1.default.fetchMaxRetrySet(shimFetchMaxRetryPrevious); + }; + while (true) { + try { + const result = yield fn(); + defer(); + return result; + } + catch (error) { + if (retryCount >= count || !requestCanBeRepeated(error)) { + defer(); + throw error; + } + retryCount++; + yield time_1.default.sleep(1 + retryCount * 3); + } + } + }); } - class FileApi { - constructor(baseDir, driver) { - this.baseDir_ = baseDir; - this.driver_ = driver; - this.logger_ = new Logger(); - this.syncTargetId_ = null; - this.tempDirName_ = null; - this.driver_.fileApi_ = this; - this.requestRepeatCount_ = null; // For testing purpose only - normally this value should come from the driver - this.remoteDateOffset_ = 0; - this.remoteDateNextCheckTime_ = 0; - this.remoteDateMutex_ = new Mutex(); - this.initialized_ = false; - } - - async initialize() { - if (this.initialized_) return; - this.initialized_ = true; - if (this.driver_.initialize) return this.driver_.initialize(this.fullPath_('')); - } - - async fetchRemoteDateOffset_() { - const tempFile = `${this.tempDirName()}/timeCheck${Math.round(Math.random() * 1000000)}.txt`; - const startTime = Date.now(); - await this.put(tempFile, 'timeCheck'); - - // Normally it should be possible to read the file back immediately but - // just in case, read it in a loop. - const loopStartTime = Date.now(); - let stat = null; - while (Date.now() - loopStartTime < 5000) { - stat = await this.stat(tempFile); - if (stat) break; - await time.msleep(200); - } - - if (!stat) throw new Error('Timed out trying to get sync target clock time'); - - this.delete(tempFile); // No need to await for this call - - const endTime = Date.now(); - const expectedTime = Math.round((endTime + startTime) / 2); - return stat.updated_time - expectedTime; - } - - // Approximates the current time on the sync target. It caches the time offset to - // improve performance. - async remoteDate() { - const shouldSyncTime = () => { - return !this.remoteDateNextCheckTime_ || Date.now() > this.remoteDateNextCheckTime_; - }; - - if (shouldSyncTime()) { - const release = await this.remoteDateMutex_.acquire(); - - try { - // Another call might have refreshed the time while we were waiting for the mutex, - // so check again if we need to refresh. - if (shouldSyncTime()) { - this.remoteDateOffset_ = await this.fetchRemoteDateOffset_(); - // The sync target clock should rarely change but the device one might, - // so we need to refresh relatively frequently. - this.remoteDateNextCheckTime_ = Date.now() + 10 * 60 * 1000; - } - } catch (error) { - logger.warn('Could not retrieve remote date - defaulting to device date:', error); - this.remoteDateOffset_ = 0; - this.remoteDateNextCheckTime_ = Date.now() + 60 * 1000; - } finally { - release(); - } - } - - return new Date(Date.now() + this.remoteDateOffset_); - } - - // Ideally all requests repeating should be done at the FileApi level to remove duplicate code in the drivers, but - // historically some drivers (eg. OneDrive) are already handling request repeating, so this is optional, per driver, - // and it defaults to no repeating. - requestRepeatCount() { - if (this.requestRepeatCount_ !== null) return this.requestRepeatCount_; - if (this.driver_.requestRepeatCount) return this.driver_.requestRepeatCount(); - return 0; - } - - lastRequests() { - return this.driver_.lastRequests ? this.driver_.lastRequests() : []; - } - - clearLastRequests() { - if (this.driver_.clearLastRequests) this.driver_.clearLastRequests(); - } - - baseDir() { - return typeof this.baseDir_ === 'function' ? this.baseDir_() : this.baseDir_; - } - - tempDirName() { - if (this.tempDirName_ === null) throw Error('Temp dir not set!'); - return this.tempDirName_; - } - - setTempDirName(v) { - this.tempDirName_ = v; - } - - fsDriver() { - return shim.fsDriver(); - } - - driver() { - return this.driver_; - } - - setSyncTargetId(v) { - this.syncTargetId_ = v; - } - - syncTargetId() { - if (this.syncTargetId_ === null) throw new Error('syncTargetId has not been set!!'); - return this.syncTargetId_; - } - - setLogger(l) { - if (!l) l = new Logger(); - this.logger_ = l; - } - - logger() { - return this.logger_; - } - - fullPath_(path) { - const output = []; - if (this.baseDir()) output.push(this.baseDir()); - if (path) output.push(path); - return output.join('/'); - } - - // DRIVER MUST RETURN PATHS RELATIVE TO `path` - // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars - async list(path = '', options = null) { - if (!options) options = {}; - if (!('includeHidden' in options)) options.includeHidden = false; - if (!('context' in options)) options.context = null; - if (!('includeDirs' in options)) options.includeDirs = true; - if (!('syncItemsOnly' in options)) options.syncItemsOnly = false; - - logger.debug(`list ${this.baseDir()}`); - - const result = await tryAndRepeat(() => this.driver_.list(this.fullPath_(path), options), this.requestRepeatCount()); - - if (!options.includeHidden) { - const temp = []; - for (let i = 0; i < result.items.length; i++) { - if (!isHidden(result.items[i].path)) temp.push(result.items[i]); - } - result.items = temp; - } - - if (!options.includeDirs) { - result.items = result.items.filter(f => !f.isDir); - } - - if (options.syncItemsOnly) { - result.items = result.items.filter(f => !f.isDir && BaseItem.isSystemPath(f.path)); - } - - return result; - } - - // Deprectated - setTimestamp(path, timestampMs) { - logger.debug(`setTimestamp ${this.fullPath_(path)}`); - return tryAndRepeat(() => this.driver_.setTimestamp(this.fullPath_(path), timestampMs), this.requestRepeatCount()); - // return this.driver_.setTimestamp(this.fullPath_(path), timestampMs); - } - - mkdir(path) { - logger.debug(`mkdir ${this.fullPath_(path)}`); - return tryAndRepeat(() => this.driver_.mkdir(this.fullPath_(path)), this.requestRepeatCount()); - } - - async stat(path) { - logger.debug(`stat ${this.fullPath_(path)}`); - - const output = await tryAndRepeat(() => this.driver_.stat(this.fullPath_(path)), this.requestRepeatCount()); - - if (!output) return output; - output.path = path; - return output; - - // return this.driver_.stat(this.fullPath_(path)).then((output) => { - // if (!output) return output; - // output.path = path; - // return output; - // }); - } - - // Returns UTF-8 encoded string by default, or a Response if `options.target = 'file'` - get(path, options = null) { - if (!options) options = {}; - if (!options.encoding) options.encoding = 'utf8'; - logger.debug(`get ${this.fullPath_(path)}`); - return tryAndRepeat(() => this.driver_.get(this.fullPath_(path), options), this.requestRepeatCount()); - } - - async put(path, content, options = null) { - logger.debug(`put ${this.fullPath_(path)}`, options); - - if (options && options.source === 'file') { - if (!(await this.fsDriver().exists(options.path))) throw new JoplinError(`File not found: ${options.path}`, 'fileNotFound'); - } - - return tryAndRepeat(() => this.driver_.put(this.fullPath_(path), content, options), this.requestRepeatCount()); - } - - delete(path) { - logger.debug(`delete ${this.fullPath_(path)}`); - return tryAndRepeat(() => this.driver_.delete(this.fullPath_(path)), this.requestRepeatCount()); - } - - // Deprectated - move(oldPath, newPath) { - logger.debug(`move ${this.fullPath_(oldPath)} => ${this.fullPath_(newPath)}`); - return tryAndRepeat(() => this.driver_.move(this.fullPath_(oldPath), this.fullPath_(newPath)), this.requestRepeatCount()); - } - - // Deprectated - format() { - return tryAndRepeat(() => this.driver_.format(), this.requestRepeatCount()); - } - - clearRoot() { - return tryAndRepeat(() => this.driver_.clearRoot(this.baseDir()), this.requestRepeatCount()); - } - - delta(path, options = null) { - logger.debug(`delta ${this.fullPath_(path)}`); - return tryAndRepeat(() => this.driver_.delta(this.fullPath_(path), options), this.requestRepeatCount()); - } + constructor(baseDir, driver) { + this.logger_ = new Logger_1.default(); + this.syncTargetId_ = null; + this.tempDirName_ = null; + this.requestRepeatCount_ = null; // For testing purpose only - normally this value should come from the driver + this.remoteDateOffset_ = 0; + this.remoteDateNextCheckTime_ = 0; + this.remoteDateMutex_ = new Mutex(); + this.initialized_ = false; + this.baseDir_ = baseDir; + this.driver_ = driver; + this.driver_.fileApi_ = this; + } + initialize() { + return __awaiter(this, void 0, void 0, function* () { + if (this.initialized_) + return; + this.initialized_ = true; + if (this.driver_.initialize) + return this.driver_.initialize(this.fullPath('')); + }); + } + fetchRemoteDateOffset_() { + return __awaiter(this, void 0, void 0, function* () { + const tempFile = `${this.tempDirName()}/timeCheck${Math.round(Math.random() * 1000000)}.txt`; + const startTime = Date.now(); + yield this.put(tempFile, 'timeCheck'); + // Normally it should be possible to read the file back immediately but + // just in case, read it in a loop. + const loopStartTime = Date.now(); + let stat = null; + while (Date.now() - loopStartTime < 5000) { + stat = yield this.stat(tempFile); + if (stat) + break; + yield time_1.default.msleep(200); + } + if (!stat) + throw new Error('Timed out trying to get sync target clock time'); + void this.delete(tempFile); // No need to await for this call + const endTime = Date.now(); + const expectedTime = Math.round((endTime + startTime) / 2); + return stat.updated_time - expectedTime; + }); + } + // Approximates the current time on the sync target. It caches the time offset to + // improve performance. + remoteDate() { + return __awaiter(this, void 0, void 0, function* () { + const shouldSyncTime = () => { + return !this.remoteDateNextCheckTime_ || Date.now() > this.remoteDateNextCheckTime_; + }; + if (shouldSyncTime()) { + const release = yield this.remoteDateMutex_.acquire(); + try { + // Another call might have refreshed the time while we were waiting for the mutex, + // so check again if we need to refresh. + if (shouldSyncTime()) { + this.remoteDateOffset_ = yield this.fetchRemoteDateOffset_(); + // The sync target clock should rarely change but the device one might, + // so we need to refresh relatively frequently. + this.remoteDateNextCheckTime_ = Date.now() + 10 * 60 * 1000; + } + } + catch (error) { + logger.warn('Could not retrieve remote date - defaulting to device date:', error); + this.remoteDateOffset_ = 0; + this.remoteDateNextCheckTime_ = Date.now() + 60 * 1000; + } + finally { + release(); + } + } + return new Date(Date.now() + this.remoteDateOffset_); + }); + } + // Ideally all requests repeating should be done at the FileApi level to remove duplicate code in the drivers, but + // historically some drivers (eg. OneDrive) are already handling request repeating, so this is optional, per driver, + // and it defaults to no repeating. + requestRepeatCount() { + if (this.requestRepeatCount_ !== null) + return this.requestRepeatCount_; + if (this.driver_.requestRepeatCount) + return this.driver_.requestRepeatCount(); + return 0; + } + lastRequests() { + return this.driver_.lastRequests ? this.driver_.lastRequests() : []; + } + clearLastRequests() { + if (this.driver_.clearLastRequests) + this.driver_.clearLastRequests(); + } + baseDir() { + return typeof this.baseDir_ === 'function' ? this.baseDir_() : this.baseDir_; + } + tempDirName() { + if (this.tempDirName_ === null) + throw Error('Temp dir not set!'); + return this.tempDirName_; + } + setTempDirName(v) { + this.tempDirName_ = v; + } + fsDriver() { + return shim_1.default.fsDriver(); + } + driver() { + return this.driver_; + } + setSyncTargetId(v) { + this.syncTargetId_ = v; + } + syncTargetId() { + if (this.syncTargetId_ === null) + throw new Error('syncTargetId has not been set!!'); + return this.syncTargetId_; + } + setLogger(l) { + if (!l) + l = new Logger_1.default(); + this.logger_ = l; + } + logger() { + return this.logger_; + } + fullPath(path) { + const output = []; + if (this.baseDir()) + output.push(this.baseDir()); + if (path) + output.push(path); + return output.join('/'); + } + // DRIVER MUST RETURN PATHS RELATIVE TO `path` + // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars + list(path = '', options = null) { + return __awaiter(this, void 0, void 0, function* () { + if (!options) + options = {}; + if (!('includeHidden' in options)) + options.includeHidden = false; + if (!('context' in options)) + options.context = null; + if (!('includeDirs' in options)) + options.includeDirs = true; + if (!('syncItemsOnly' in options)) + options.syncItemsOnly = false; + logger.debug(`list ${this.baseDir()}`); + const result = yield tryAndRepeat(() => this.driver_.list(this.fullPath(path), options), this.requestRepeatCount()); + if (!options.includeHidden) { + const temp = []; + for (let i = 0; i < result.items.length; i++) { + if (!isHidden(result.items[i].path)) + temp.push(result.items[i]); + } + result.items = temp; + } + if (!options.includeDirs) { + result.items = result.items.filter((f) => !f.isDir); + } + if (options.syncItemsOnly) { + result.items = result.items.filter((f) => !f.isDir && BaseItem_1.default.isSystemPath(f.path)); + } + return result; + }); + } + // Deprectated + setTimestamp(path, timestampMs) { + logger.debug(`setTimestamp ${this.fullPath(path)}`); + return tryAndRepeat(() => this.driver_.setTimestamp(this.fullPath(path), timestampMs), this.requestRepeatCount()); + // return this.driver_.setTimestamp(this.fullPath(path), timestampMs); + } + mkdir(path) { + logger.debug(`mkdir ${this.fullPath(path)}`); + return tryAndRepeat(() => this.driver_.mkdir(this.fullPath(path)), this.requestRepeatCount()); + } + stat(path) { + return __awaiter(this, void 0, void 0, function* () { + logger.debug(`stat ${this.fullPath(path)}`); + const output = yield tryAndRepeat(() => this.driver_.stat(this.fullPath(path)), this.requestRepeatCount()); + if (!output) + return output; + output.path = path; + return output; + // return this.driver_.stat(this.fullPath(path)).then((output) => { + // if (!output) return output; + // output.path = path; + // return output; + // }); + }); + } + // Returns UTF-8 encoded string by default, or a Response if `options.target = 'file'` + get(path, options = null) { + if (!options) + options = {}; + if (!options.encoding) + options.encoding = 'utf8'; + logger.debug(`get ${this.fullPath(path)}`); + return tryAndRepeat(() => this.driver_.get(this.fullPath(path), options), this.requestRepeatCount()); + } + put(path, content, options = null) { + return __awaiter(this, void 0, void 0, function* () { + logger.debug(`put ${this.fullPath(path)}`, options); + if (options && options.source === 'file') { + if (!(yield this.fsDriver().exists(options.path))) + throw new JoplinError(`File not found: ${options.path}`, 'fileNotFound'); + } + return tryAndRepeat(() => this.driver_.put(this.fullPath(path), content, options), this.requestRepeatCount()); + }); + } + delete(path) { + logger.debug(`delete ${this.fullPath(path)}`); + return tryAndRepeat(() => this.driver_.delete(this.fullPath(path)), this.requestRepeatCount()); + } + // Deprectated + move(oldPath, newPath) { + logger.debug(`move ${this.fullPath(oldPath)} => ${this.fullPath(newPath)}`); + return tryAndRepeat(() => this.driver_.move(this.fullPath(oldPath), this.fullPath(newPath)), this.requestRepeatCount()); + } + // Deprectated + format() { + return tryAndRepeat(() => this.driver_.format(), this.requestRepeatCount()); + } + clearRoot() { + return tryAndRepeat(() => this.driver_.clearRoot(this.baseDir()), this.requestRepeatCount()); + } + delta(path, options = null) { + logger.debug(`delta ${this.fullPath(path)}`); + return tryAndRepeat(() => this.driver_.delta(this.fullPath(path), options), this.requestRepeatCount()); + } } - +exports.FileApi = FileApi; function basicDeltaContextFromOptions_(options) { - const output = { - timestamp: 0, - filesAtTimestamp: [], - statsCache: null, - statIdsCache: null, - deletedItemsProcessed: false, - }; - - if (!options || !options.context) return output; - - const d = new Date(options.context.timestamp); - - output.timestamp = isNaN(d.getTime()) ? 0 : options.context.timestamp; - output.filesAtTimestamp = Array.isArray(options.context.filesAtTimestamp) ? options.context.filesAtTimestamp.slice() : []; - output.statsCache = options.context && options.context.statsCache ? options.context.statsCache : null; - output.statIdsCache = options.context && options.context.statIdsCache ? options.context.statIdsCache : null; - output.deletedItemsProcessed = options.context && 'deletedItemsProcessed' in options.context ? options.context.deletedItemsProcessed : false; - - return output; + const output = { + timestamp: 0, + filesAtTimestamp: [], + statsCache: null, + statIdsCache: null, + deletedItemsProcessed: false, + }; + if (!options || !options.context) + return output; + const d = new Date(options.context.timestamp); + output.timestamp = isNaN(d.getTime()) ? 0 : options.context.timestamp; + output.filesAtTimestamp = Array.isArray(options.context.filesAtTimestamp) ? options.context.filesAtTimestamp.slice() : []; + output.statsCache = options.context && options.context.statsCache ? options.context.statsCache : null; + output.statIdsCache = options.context && options.context.statIdsCache ? options.context.statIdsCache : null; + output.deletedItemsProcessed = options.context && 'deletedItemsProcessed' in options.context ? options.context.deletedItemsProcessed : false; + return output; } - // This is the basic delta algorithm, which can be used in case the cloud service does not have // a built-in delta API. OneDrive and Dropbox have one for example, but Nextcloud and obviously // the file system do not. -async function basicDelta(path, getDirStatFn, options) { - const outputLimit = 50; - const itemIds = await options.allItemIdsHandler(); - if (!Array.isArray(itemIds)) throw new Error('Delta API not supported - local IDs must be provided'); - - const logger = options && options.logger ? options.logger : new Logger(); - - const context = basicDeltaContextFromOptions_(options); - - if (context.timestamp > Date.now()) { - logger.warn(`BasicDelta: Context timestamp is greater than current time: ${context.timestamp}`); - logger.warn('BasicDelta: Sync will continue but it is likely that nothing will be synced'); - } - - const newContext = { - timestamp: context.timestamp, - filesAtTimestamp: context.filesAtTimestamp.slice(), - statsCache: context.statsCache, - statIdsCache: context.statIdsCache, - deletedItemsProcessed: context.deletedItemsProcessed, - }; - - // Stats are cached until all items have been processed (until hasMore is false) - if (newContext.statsCache === null) { - newContext.statsCache = await getDirStatFn(path); - newContext.statsCache.sort(function(a, b) { - return a.updated_time - b.updated_time; - }); - newContext.statIdsCache = newContext.statsCache.filter(item => BaseItem.isSystemPath(item.path)).map(item => BaseItem.pathToId(item.path)); - newContext.statIdsCache.sort(); // Items must be sorted to use binary search below - } - - let output = []; - - const updateReport = { - timestamp: context.timestamp, - older: 0, - newer: 0, - equal: 0, - }; - - // Find out which files have been changed since the last time. Note that we keep - // both the timestamp of the most recent change, *and* the items that exactly match - // this timestamp. This to handle cases where an item is modified while this delta - // function is running. For example: - // t0: Item 1 is changed - // t0: Sync items - run delta function - // t0: While delta() is running, modify Item 2 - // Since item 2 was modified within the same millisecond, it would be skipped in the - // next sync if we relied exclusively on a timestamp. - for (let i = 0; i < newContext.statsCache.length; i++) { - const stat = newContext.statsCache[i]; - - if (stat.isDir) continue; - - if (stat.updated_time < context.timestamp) { - updateReport.older++; - continue; - } - - // Special case for items that exactly match the timestamp - if (stat.updated_time === context.timestamp) { - if (context.filesAtTimestamp.indexOf(stat.path) >= 0) { - updateReport.equal++; - continue; - } - } - - if (stat.updated_time > newContext.timestamp) { - newContext.timestamp = stat.updated_time; - newContext.filesAtTimestamp = []; - updateReport.newer++; - } - - newContext.filesAtTimestamp.push(stat.path); - output.push(stat); - - if (output.length >= outputLimit) break; - } - - logger.info(`BasicDelta: Report: ${JSON.stringify(updateReport)}`); - - if (!newContext.deletedItemsProcessed) { - // Find out which items have been deleted on the sync target by comparing the items - // we have to the items on the target. - // Note that when deleted items are processed it might result in the output having - // more items than outputLimit. This is acceptable since delete operations are cheap. - const deletedItems = []; - for (let i = 0; i < itemIds.length; i++) { - const itemId = itemIds[i]; - - if (ArrayUtils.binarySearch(newContext.statIdsCache, itemId) < 0) { - deletedItems.push({ - path: BaseItem.systemPath(itemId), - isDeleted: true, - }); - } - } - - const percentDeleted = itemIds.length ? deletedItems.length / itemIds.length : 0; - - // If more than 90% of the notes are going to be deleted, it's most likely a - // configuration error or bug. For example, if the user moves their Nextcloud - // directory, or if a network drive gets disconnected and returns an empty dir - // instead of an error. In that case, we don't wipe out the user data, unless - // they have switched off the fail-safe. - if (options.wipeOutFailSafe && percentDeleted >= 0.90) throw new JoplinError(sprintf('Fail-safe: Sync was interrupted because %d%% of the data (%d items) is about to be deleted. To override this behaviour disable the fail-safe in the sync settings.', Math.round(percentDeleted * 100), deletedItems.length), 'failSafe'); - - output = output.concat(deletedItems); - } - - newContext.deletedItemsProcessed = true; - - const hasMore = output.length >= outputLimit; - - if (!hasMore) { - // Clear temporary info from context. It's especially important to remove deletedItemsProcessed - // so that they are processed again on the next sync. - newContext.statsCache = null; - newContext.statIdsCache = null; - delete newContext.deletedItemsProcessed; - } - - return { - hasMore: hasMore, - context: newContext, - items: output, - }; +function basicDelta(path, getDirStatFn, options) { + return __awaiter(this, void 0, void 0, function* () { + const outputLimit = 50; + const itemIds = yield options.allItemIdsHandler(); + if (!Array.isArray(itemIds)) + throw new Error('Delta API not supported - local IDs must be provided'); + const logger = options && options.logger ? options.logger : new Logger_1.default(); + const context = basicDeltaContextFromOptions_(options); + if (context.timestamp > Date.now()) { + logger.warn(`BasicDelta: Context timestamp is greater than current time: ${context.timestamp}`); + logger.warn('BasicDelta: Sync will continue but it is likely that nothing will be synced'); + } + const newContext = { + timestamp: context.timestamp, + filesAtTimestamp: context.filesAtTimestamp.slice(), + statsCache: context.statsCache, + statIdsCache: context.statIdsCache, + deletedItemsProcessed: context.deletedItemsProcessed, + }; + // Stats are cached until all items have been processed (until hasMore is false) + if (newContext.statsCache === null) { + newContext.statsCache = yield getDirStatFn(path); + newContext.statsCache.sort(function (a, b) { + return a.updated_time - b.updated_time; + }); + newContext.statIdsCache = newContext.statsCache.filter((item) => BaseItem_1.default.isSystemPath(item.path)).map((item) => BaseItem_1.default.pathToId(item.path)); + newContext.statIdsCache.sort(); // Items must be sorted to use binary search below + } + let output = []; + const updateReport = { + timestamp: context.timestamp, + older: 0, + newer: 0, + equal: 0, + }; + // Find out which files have been changed since the last time. Note that we keep + // both the timestamp of the most recent change, *and* the items that exactly match + // this timestamp. This to handle cases where an item is modified while this delta + // function is running. For example: + // t0: Item 1 is changed + // t0: Sync items - run delta function + // t0: While delta() is running, modify Item 2 + // Since item 2 was modified within the same millisecond, it would be skipped in the + // next sync if we relied exclusively on a timestamp. + for (let i = 0; i < newContext.statsCache.length; i++) { + const stat = newContext.statsCache[i]; + if (stat.isDir) + continue; + if (stat.updated_time < context.timestamp) { + updateReport.older++; + continue; + } + // Special case for items that exactly match the timestamp + if (stat.updated_time === context.timestamp) { + if (context.filesAtTimestamp.indexOf(stat.path) >= 0) { + updateReport.equal++; + continue; + } + } + if (stat.updated_time > newContext.timestamp) { + newContext.timestamp = stat.updated_time; + newContext.filesAtTimestamp = []; + updateReport.newer++; + } + newContext.filesAtTimestamp.push(stat.path); + output.push(stat); + if (output.length >= outputLimit) + break; + } + logger.info(`BasicDelta: Report: ${JSON.stringify(updateReport)}`); + if (!newContext.deletedItemsProcessed) { + // Find out which items have been deleted on the sync target by comparing the items + // we have to the items on the target. + // Note that when deleted items are processed it might result in the output having + // more items than outputLimit. This is acceptable since delete operations are cheap. + const deletedItems = []; + for (let i = 0; i < itemIds.length; i++) { + const itemId = itemIds[i]; + if (ArrayUtils.binarySearch(newContext.statIdsCache, itemId) < 0) { + deletedItems.push({ + path: BaseItem_1.default.systemPath(itemId), + isDeleted: true, + }); + } + } + const percentDeleted = itemIds.length ? deletedItems.length / itemIds.length : 0; + // If more than 90% of the notes are going to be deleted, it's most likely a + // configuration error or bug. For example, if the user moves their Nextcloud + // directory, or if a network drive gets disconnected and returns an empty dir + // instead of an error. In that case, we don't wipe out the user data, unless + // they have switched off the fail-safe. + if (options.wipeOutFailSafe && percentDeleted >= 0.90) + throw new JoplinError(sprintf('Fail-safe: Sync was interrupted because %d%% of the data (%d items) is about to be deleted. To override this behaviour disable the fail-safe in the sync settings.', Math.round(percentDeleted * 100), deletedItems.length), 'failSafe'); + output = output.concat(deletedItems); + } + newContext.deletedItemsProcessed = true; + const hasMore = output.length >= outputLimit; + if (!hasMore) { + // Clear temporary info from context. It's especially important to remove deletedItemsProcessed + // so that they are processed again on the next sync. + newContext.statsCache = null; + newContext.statIdsCache = null; + delete newContext.deletedItemsProcessed; + } + return { + hasMore: hasMore, + context: newContext, + items: output, + }; + }); } - -module.exports = { FileApi, basicDelta }; +exports.basicDelta = basicDelta; +//# sourceMappingURL=file-api.js.map \ No newline at end of file diff --git a/packages/lib/file-api.ts b/packages/lib/file-api.ts new file mode 100644 index 0000000000..64e8cb891c --- /dev/null +++ b/packages/lib/file-api.ts @@ -0,0 +1,457 @@ +import Logger from './Logger'; +import shim from './shim'; +import BaseItem from './models/BaseItem'; +import time from './time'; + +const { isHidden } = require('./path-utils'); +const JoplinError = require('./JoplinError'); +const ArrayUtils = require('./ArrayUtils'); +const { sprintf } = require('sprintf-js'); +const Mutex = require('async-mutex').Mutex; + +const logger = Logger.create('FileApi'); + +function requestCanBeRepeated(error: any) { + const errorCode = typeof error === 'object' && error.code ? error.code : null; + + // The target is explicitely rejecting the item so repeating wouldn't make a difference. + if (errorCode === 'rejectedByTarget') return false; + + // We don't repeat failSafe errors because it's an indication of an issue at the + // server-level issue which usually cannot be fixed by repeating the request. + // Also we print the previous requests and responses to the log in this case, + // so not repeating means there will be less noise in the log. + if (errorCode === 'failSafe') return false; + + return true; +} + +async function tryAndRepeat(fn: Function, count: number) { + let retryCount = 0; + + // Don't use internal fetch retry mechanim since we + // are already retrying here. + const shimFetchMaxRetryPrevious = shim.fetchMaxRetrySet(0); + const defer = () => { + shim.fetchMaxRetrySet(shimFetchMaxRetryPrevious); + }; + + while (true) { + try { + const result = await fn(); + defer(); + return result; + } catch (error) { + if (retryCount >= count || !requestCanBeRepeated(error)) { + defer(); + throw error; + } + retryCount++; + await time.sleep(1 + retryCount * 3); + } + } +} + +class FileApi { + + private baseDir_: any; + private driver_: any; + private logger_: Logger = new Logger(); + private syncTargetId_: number = null; + private tempDirName_: string = null; + public requestRepeatCount_: number = null; // For testing purpose only - normally this value should come from the driver + private remoteDateOffset_ = 0; + private remoteDateNextCheckTime_ = 0; + private remoteDateMutex_ = new Mutex(); + private initialized_ = false; + + constructor(baseDir: string | Function, driver: any) { + this.baseDir_ = baseDir; + this.driver_ = driver; + this.driver_.fileApi_ = this; + } + + async initialize() { + if (this.initialized_) return; + this.initialized_ = true; + if (this.driver_.initialize) return this.driver_.initialize(this.fullPath('')); + } + + async fetchRemoteDateOffset_() { + const tempFile = `${this.tempDirName()}/timeCheck${Math.round(Math.random() * 1000000)}.txt`; + const startTime = Date.now(); + await this.put(tempFile, 'timeCheck'); + + // Normally it should be possible to read the file back immediately but + // just in case, read it in a loop. + const loopStartTime = Date.now(); + let stat = null; + while (Date.now() - loopStartTime < 5000) { + stat = await this.stat(tempFile); + if (stat) break; + await time.msleep(200); + } + + if (!stat) throw new Error('Timed out trying to get sync target clock time'); + + void this.delete(tempFile); // No need to await for this call + + const endTime = Date.now(); + const expectedTime = Math.round((endTime + startTime) / 2); + return stat.updated_time - expectedTime; + } + + // Approximates the current time on the sync target. It caches the time offset to + // improve performance. + async remoteDate() { + const shouldSyncTime = () => { + return !this.remoteDateNextCheckTime_ || Date.now() > this.remoteDateNextCheckTime_; + }; + + if (shouldSyncTime()) { + const release = await this.remoteDateMutex_.acquire(); + + try { + // Another call might have refreshed the time while we were waiting for the mutex, + // so check again if we need to refresh. + if (shouldSyncTime()) { + this.remoteDateOffset_ = await this.fetchRemoteDateOffset_(); + // The sync target clock should rarely change but the device one might, + // so we need to refresh relatively frequently. + this.remoteDateNextCheckTime_ = Date.now() + 10 * 60 * 1000; + } + } catch (error) { + logger.warn('Could not retrieve remote date - defaulting to device date:', error); + this.remoteDateOffset_ = 0; + this.remoteDateNextCheckTime_ = Date.now() + 60 * 1000; + } finally { + release(); + } + } + + return new Date(Date.now() + this.remoteDateOffset_); + } + + // Ideally all requests repeating should be done at the FileApi level to remove duplicate code in the drivers, but + // historically some drivers (eg. OneDrive) are already handling request repeating, so this is optional, per driver, + // and it defaults to no repeating. + requestRepeatCount() { + if (this.requestRepeatCount_ !== null) return this.requestRepeatCount_; + if (this.driver_.requestRepeatCount) return this.driver_.requestRepeatCount(); + return 0; + } + + lastRequests() { + return this.driver_.lastRequests ? this.driver_.lastRequests() : []; + } + + clearLastRequests() { + if (this.driver_.clearLastRequests) this.driver_.clearLastRequests(); + } + + baseDir() { + return typeof this.baseDir_ === 'function' ? this.baseDir_() : this.baseDir_; + } + + tempDirName() { + if (this.tempDirName_ === null) throw Error('Temp dir not set!'); + return this.tempDirName_; + } + + setTempDirName(v: string) { + this.tempDirName_ = v; + } + + fsDriver() { + return shim.fsDriver(); + } + + driver() { + return this.driver_; + } + + setSyncTargetId(v: number) { + this.syncTargetId_ = v; + } + + syncTargetId() { + if (this.syncTargetId_ === null) throw new Error('syncTargetId has not been set!!'); + return this.syncTargetId_; + } + + setLogger(l: Logger) { + if (!l) l = new Logger(); + this.logger_ = l; + } + + logger() { + return this.logger_; + } + + fullPath(path: string) { + const output = []; + if (this.baseDir()) output.push(this.baseDir()); + if (path) output.push(path); + return output.join('/'); + } + + // DRIVER MUST RETURN PATHS RELATIVE TO `path` + // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars + async list(path = '', options: any = null) { + if (!options) options = {}; + if (!('includeHidden' in options)) options.includeHidden = false; + if (!('context' in options)) options.context = null; + if (!('includeDirs' in options)) options.includeDirs = true; + if (!('syncItemsOnly' in options)) options.syncItemsOnly = false; + + logger.debug(`list ${this.baseDir()}`); + + const result = await tryAndRepeat(() => this.driver_.list(this.fullPath(path), options), this.requestRepeatCount()); + + if (!options.includeHidden) { + const temp = []; + for (let i = 0; i < result.items.length; i++) { + if (!isHidden(result.items[i].path)) temp.push(result.items[i]); + } + result.items = temp; + } + + if (!options.includeDirs) { + result.items = result.items.filter((f: any) => !f.isDir); + } + + if (options.syncItemsOnly) { + result.items = result.items.filter((f: any) => !f.isDir && BaseItem.isSystemPath(f.path)); + } + + return result; + } + + // Deprectated + setTimestamp(path: string, timestampMs: number) { + logger.debug(`setTimestamp ${this.fullPath(path)}`); + return tryAndRepeat(() => this.driver_.setTimestamp(this.fullPath(path), timestampMs), this.requestRepeatCount()); + // return this.driver_.setTimestamp(this.fullPath(path), timestampMs); + } + + mkdir(path: string) { + logger.debug(`mkdir ${this.fullPath(path)}`); + return tryAndRepeat(() => this.driver_.mkdir(this.fullPath(path)), this.requestRepeatCount()); + } + + async stat(path: string) { + logger.debug(`stat ${this.fullPath(path)}`); + + const output = await tryAndRepeat(() => this.driver_.stat(this.fullPath(path)), this.requestRepeatCount()); + + if (!output) return output; + output.path = path; + return output; + + // return this.driver_.stat(this.fullPath(path)).then((output) => { + // if (!output) return output; + // output.path = path; + // return output; + // }); + } + + // Returns UTF-8 encoded string by default, or a Response if `options.target = 'file'` + get(path: string, options: any = null) { + if (!options) options = {}; + if (!options.encoding) options.encoding = 'utf8'; + logger.debug(`get ${this.fullPath(path)}`); + return tryAndRepeat(() => this.driver_.get(this.fullPath(path), options), this.requestRepeatCount()); + } + + async put(path: string, content: any, options: any = null) { + logger.debug(`put ${this.fullPath(path)}`, options); + + if (options && options.source === 'file') { + if (!(await this.fsDriver().exists(options.path))) throw new JoplinError(`File not found: ${options.path}`, 'fileNotFound'); + } + + return tryAndRepeat(() => this.driver_.put(this.fullPath(path), content, options), this.requestRepeatCount()); + } + + delete(path: string) { + logger.debug(`delete ${this.fullPath(path)}`); + return tryAndRepeat(() => this.driver_.delete(this.fullPath(path)), this.requestRepeatCount()); + } + + // Deprectated + move(oldPath: string, newPath: string) { + logger.debug(`move ${this.fullPath(oldPath)} => ${this.fullPath(newPath)}`); + return tryAndRepeat(() => this.driver_.move(this.fullPath(oldPath), this.fullPath(newPath)), this.requestRepeatCount()); + } + + // Deprectated + format() { + return tryAndRepeat(() => this.driver_.format(), this.requestRepeatCount()); + } + + clearRoot() { + return tryAndRepeat(() => this.driver_.clearRoot(this.baseDir()), this.requestRepeatCount()); + } + + delta(path: string, options: any = null) { + logger.debug(`delta ${this.fullPath(path)}`); + return tryAndRepeat(() => this.driver_.delta(this.fullPath(path), options), this.requestRepeatCount()); + } +} + +function basicDeltaContextFromOptions_(options: any) { + const output: any = { + timestamp: 0, + filesAtTimestamp: [], + statsCache: null, + statIdsCache: null, + deletedItemsProcessed: false, + }; + + if (!options || !options.context) return output; + + const d = new Date(options.context.timestamp); + + output.timestamp = isNaN(d.getTime()) ? 0 : options.context.timestamp; + output.filesAtTimestamp = Array.isArray(options.context.filesAtTimestamp) ? options.context.filesAtTimestamp.slice() : []; + output.statsCache = options.context && options.context.statsCache ? options.context.statsCache : null; + output.statIdsCache = options.context && options.context.statIdsCache ? options.context.statIdsCache : null; + output.deletedItemsProcessed = options.context && 'deletedItemsProcessed' in options.context ? options.context.deletedItemsProcessed : false; + + return output; +} + +// This is the basic delta algorithm, which can be used in case the cloud service does not have +// a built-in delta API. OneDrive and Dropbox have one for example, but Nextcloud and obviously +// the file system do not. +async function basicDelta(path: string, getDirStatFn: Function, options: any) { + const outputLimit = 50; + const itemIds = await options.allItemIdsHandler(); + if (!Array.isArray(itemIds)) throw new Error('Delta API not supported - local IDs must be provided'); + + const logger = options && options.logger ? options.logger : new Logger(); + + const context = basicDeltaContextFromOptions_(options); + + if (context.timestamp > Date.now()) { + logger.warn(`BasicDelta: Context timestamp is greater than current time: ${context.timestamp}`); + logger.warn('BasicDelta: Sync will continue but it is likely that nothing will be synced'); + } + + const newContext = { + timestamp: context.timestamp, + filesAtTimestamp: context.filesAtTimestamp.slice(), + statsCache: context.statsCache, + statIdsCache: context.statIdsCache, + deletedItemsProcessed: context.deletedItemsProcessed, + }; + + // Stats are cached until all items have been processed (until hasMore is false) + if (newContext.statsCache === null) { + newContext.statsCache = await getDirStatFn(path); + newContext.statsCache.sort(function(a: any, b: any) { + return a.updated_time - b.updated_time; + }); + newContext.statIdsCache = newContext.statsCache.filter((item: any) => BaseItem.isSystemPath(item.path)).map((item: any) => BaseItem.pathToId(item.path)); + newContext.statIdsCache.sort(); // Items must be sorted to use binary search below + } + + let output = []; + + const updateReport = { + timestamp: context.timestamp, + older: 0, + newer: 0, + equal: 0, + }; + + // Find out which files have been changed since the last time. Note that we keep + // both the timestamp of the most recent change, *and* the items that exactly match + // this timestamp. This to handle cases where an item is modified while this delta + // function is running. For example: + // t0: Item 1 is changed + // t0: Sync items - run delta function + // t0: While delta() is running, modify Item 2 + // Since item 2 was modified within the same millisecond, it would be skipped in the + // next sync if we relied exclusively on a timestamp. + for (let i = 0; i < newContext.statsCache.length; i++) { + const stat = newContext.statsCache[i]; + + if (stat.isDir) continue; + + if (stat.updated_time < context.timestamp) { + updateReport.older++; + continue; + } + + // Special case for items that exactly match the timestamp + if (stat.updated_time === context.timestamp) { + if (context.filesAtTimestamp.indexOf(stat.path) >= 0) { + updateReport.equal++; + continue; + } + } + + if (stat.updated_time > newContext.timestamp) { + newContext.timestamp = stat.updated_time; + newContext.filesAtTimestamp = []; + updateReport.newer++; + } + + newContext.filesAtTimestamp.push(stat.path); + output.push(stat); + + if (output.length >= outputLimit) break; + } + + logger.info(`BasicDelta: Report: ${JSON.stringify(updateReport)}`); + + if (!newContext.deletedItemsProcessed) { + // Find out which items have been deleted on the sync target by comparing the items + // we have to the items on the target. + // Note that when deleted items are processed it might result in the output having + // more items than outputLimit. This is acceptable since delete operations are cheap. + const deletedItems = []; + for (let i = 0; i < itemIds.length; i++) { + const itemId = itemIds[i]; + + if (ArrayUtils.binarySearch(newContext.statIdsCache, itemId) < 0) { + deletedItems.push({ + path: BaseItem.systemPath(itemId), + isDeleted: true, + }); + } + } + + const percentDeleted = itemIds.length ? deletedItems.length / itemIds.length : 0; + + // If more than 90% of the notes are going to be deleted, it's most likely a + // configuration error or bug. For example, if the user moves their Nextcloud + // directory, or if a network drive gets disconnected and returns an empty dir + // instead of an error. In that case, we don't wipe out the user data, unless + // they have switched off the fail-safe. + if (options.wipeOutFailSafe && percentDeleted >= 0.90) throw new JoplinError(sprintf('Fail-safe: Sync was interrupted because %d%% of the data (%d items) is about to be deleted. To override this behaviour disable the fail-safe in the sync settings.', Math.round(percentDeleted * 100), deletedItems.length), 'failSafe'); + + output = output.concat(deletedItems); + } + + newContext.deletedItemsProcessed = true; + + const hasMore = output.length >= outputLimit; + + if (!hasMore) { + // Clear temporary info from context. It's especially important to remove deletedItemsProcessed + // so that they are processed again on the next sync. + newContext.statsCache = null; + newContext.statIdsCache = null; + delete newContext.deletedItemsProcessed; + } + + return { + hasMore: hasMore, + context: newContext, + items: output, + }; +} + +export { FileApi, basicDelta }; diff --git a/packages/lib/models/BaseItem.ts b/packages/lib/models/BaseItem.ts index 346c47c328..4da11900d8 100644 --- a/packages/lib/models/BaseItem.ts +++ b/packages/lib/models/BaseItem.ts @@ -6,7 +6,7 @@ import time from '../time'; import markdownUtils from '../markdownUtils'; import { _ } from '../locale'; -const { Database } = require('../database.js'); +import Database from '../database'; import ItemChange from './ItemChange'; const JoplinError = require('../JoplinError.js'); const { sprintf } = require('sprintf-js'); @@ -115,7 +115,7 @@ export default class BaseItem extends BaseModel { return r.total; } - static systemPath(itemOrId: any, extension: string = null) { + public static systemPath(itemOrId: any, extension: string = null) { if (extension === null) extension = 'md'; if (typeof itemOrId === 'string') return `${itemOrId}.${extension}`; @@ -225,7 +225,7 @@ export default class BaseItem extends BaseModel { // Don't create a deleted_items entry when conflicted notes are deleted // since no other client have (or should have) them. - let conflictNoteIds = []; + let conflictNoteIds: string[] = []; if (this.modelType() == BaseModel.TYPE_NOTE) { const conflictNotes = await this.db().selectAll(`SELECT id FROM notes WHERE id IN ("${ids.join('","')}") AND is_conflict = 1`); conflictNoteIds = conflictNotes.map((n: NoteEntity) => { diff --git a/packages/lib/models/Folder.ts b/packages/lib/models/Folder.ts index e75a480f30..4b25324d4f 100644 --- a/packages/lib/models/Folder.ts +++ b/packages/lib/models/Folder.ts @@ -4,7 +4,7 @@ import time from '../time'; import { _ } from '../locale'; import Note from './Note'; -const { Database } = require('../database.js'); +import Database from '../database'; import BaseItem from './BaseItem'; const { substrWithEllipsis } = require('../string-utils.js'); @@ -107,7 +107,7 @@ export default class Folder extends BaseItem { return 'c04f1c7c04f1c7c04f1c7c04f1c7c04f'; } - static conflictFolder() { + static conflictFolder(): FolderEntity { return { type_: this.TYPE_FOLDER, id: this.conflictFolderId(), @@ -380,8 +380,8 @@ export default class Folder extends BaseItem { return output; } - static load(id: string) { - if (id == this.conflictFolderId()) return this.conflictFolder(); + static load(id: string, _options: any = null): Promise { + if (id == this.conflictFolderId()) return Promise.resolve(this.conflictFolder()); return super.load(id); } diff --git a/packages/lib/models/Note.ts b/packages/lib/models/Note.ts index ffda83a8e6..46717db9dd 100644 --- a/packages/lib/models/Note.ts +++ b/packages/lib/models/Note.ts @@ -110,7 +110,7 @@ export default class Note extends BaseItem { return BaseModel.TYPE_NOTE; } - static linkedItemIds(body: string) { + static linkedItemIds(body: string): string[] { if (!body || body.length <= 32) return []; const links = urlUtils.extractResourceUrls(body); @@ -319,7 +319,8 @@ export default class Note extends BaseItem { static previewFieldsSql(fields: string[] = null) { if (fields === null) fields = this.previewFields(); - return this.db().escapeFields(fields).join(','); + const escaped = this.db().escapeFields(fields); + return Array.isArray(escaped) ? escaped.join(',') : escaped; } static async loadFolderNoteByField(folderId: string, field: string, value: any) { diff --git a/packages/lib/models/NoteResource.ts b/packages/lib/models/NoteResource.ts index b704e52d56..c86456d774 100644 --- a/packages/lib/models/NoteResource.ts +++ b/packages/lib/models/NoteResource.ts @@ -1,4 +1,5 @@ import BaseModel from '../BaseModel'; +import { SqlQuery } from '../database'; // - If is_associated = 1, note_resources indicates which note_id is currently associated with the given resource_id // - If is_associated = 0, note_resources indicates which note_id *was* associated with the given resource_id @@ -14,6 +15,27 @@ export default class NoteResource extends BaseModel { return BaseModel.TYPE_NOTE_RESOURCE; } + public static async applySharedStatusToLinkedResources() { + const queries: SqlQuery[] = []; + + queries.push({ sql: ` + UPDATE resources + SET is_shared = 0 + ` }); + + queries.push({ sql: ` + UPDATE resources + SET is_shared = 1 + WHERE id IN ( + SELECT DISTINCT note_resources.resource_id + FROM notes JOIN note_resources ON notes.id = note_resources.note_id + WHERE notes.is_shared = 1 + ) + ` }); + + await this.db().transactionExecBatch(queries); + } + static async associatedNoteIds(resourceId: string): Promise { const rows = await this.modelSelectAll('SELECT note_id FROM note_resources WHERE resource_id = ? AND is_associated = 1', [resourceId]); return rows.map((r: any) => r.note_id); diff --git a/packages/lib/models/Resource.ts b/packages/lib/models/Resource.ts index 39f9778654..0f79b20d5d 100644 --- a/packages/lib/models/Resource.ts +++ b/packages/lib/models/Resource.ts @@ -42,11 +42,15 @@ export default class Resource extends BaseItem { return imageMimeTypes.indexOf(type.toLowerCase()) >= 0; } - static fetchStatuses(resourceIds: string[]) { - if (!resourceIds.length) return []; + static fetchStatuses(resourceIds: string[]): Promise { + if (!resourceIds.length) return Promise.resolve([]); return this.db().selectAll(`SELECT resource_id, fetch_status FROM resource_local_states WHERE resource_id IN ("${resourceIds.join('","')}")`); } + public static sharedResourceIds(): Promise { + return this.db().selectAllFields('SELECT id FROM resources WHERE is_shared = 1', {}, 'id'); + } + static errorFetchStatuses() { return this.db().selectAll(` SELECT title AS resource_title, resource_id, fetch_error diff --git a/packages/lib/models/ResourceLocalState.ts b/packages/lib/models/ResourceLocalState.ts index 7439ea2c36..e647b40e9c 100644 --- a/packages/lib/models/ResourceLocalState.ts +++ b/packages/lib/models/ResourceLocalState.ts @@ -1,6 +1,6 @@ import BaseModel from '../BaseModel'; import { ResourceLocalStateEntity } from '../services/database/types'; -const { Database } = require('../database.js'); +import Database from '../database'; export default class ResourceLocalState extends BaseModel { static tableName() { diff --git a/packages/lib/models/Setting.ts b/packages/lib/models/Setting.ts index 12f5b862cf..dac6967a2b 100644 --- a/packages/lib/models/Setting.ts +++ b/packages/lib/models/Setting.ts @@ -3,7 +3,7 @@ import { _, supportedLocalesToLanguages, defaultLocale } from '../locale'; import { ltrimSlashes } from '../path-utils'; import eventManager from '../eventManager'; import BaseModel from '../BaseModel'; -const { Database } = require('../database.js'); +import Database from '../database'; const SyncTargetRegistry = require('../SyncTargetRegistry.js'); import time from '../time'; const { sprintf } = require('sprintf-js'); diff --git a/packages/lib/registry.js b/packages/lib/registry.js index c92f02bd06..8408e8bf71 100644 --- a/packages/lib/registry.js +++ b/packages/lib/registry.js @@ -1,233 +1,227 @@ -const Logger = require('./Logger').default; -const Setting = require('./models/Setting').default; -const shim = require('./shim').default; +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.reg = void 0; +const Logger_1 = require("./Logger"); +const Setting_1 = require("./models/Setting"); +const shim_1 = require("./shim"); const SyncTargetRegistry = require('./SyncTargetRegistry.js'); - -const reg = {}; - -reg.syncTargets_ = {}; - -reg.logger = () => { - if (!reg.logger_) { - // console.warn('Calling logger before it is initialized'); - return new Logger(); - } - - return reg.logger_; -}; - -reg.setLogger = l => { - reg.logger_ = l; -}; - -reg.setShowErrorMessageBoxHandler = v => { - reg.showErrorMessageBoxHandler_ = v; -}; - -reg.showErrorMessageBox = message => { - if (!reg.showErrorMessageBoxHandler_) return; - reg.showErrorMessageBoxHandler_(message); -}; - -reg.resetSyncTarget = (syncTargetId = null) => { - if (syncTargetId === null) syncTargetId = Setting.value('sync.target'); - delete reg.syncTargets_[syncTargetId]; -}; - -reg.syncTargetNextcloud = () => { - return reg.syncTarget(SyncTargetRegistry.nameToId('nextcloud')); -}; - -reg.syncTarget = (syncTargetId = null) => { - if (syncTargetId === null) syncTargetId = Setting.value('sync.target'); - if (reg.syncTargets_[syncTargetId]) return reg.syncTargets_[syncTargetId]; - - const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId); - if (!reg.db()) throw new Error('Cannot initialize sync without a db'); - - const target = new SyncTargetClass(reg.db()); - target.setLogger(reg.logger()); - reg.syncTargets_[syncTargetId] = target; - return target; -}; - -// This can be used when some data has been modified and we want to make -// sure it gets synced. So we wait for the current sync operation to -// finish (if one is running), then we trigger a sync just after. -reg.waitForSyncFinishedThenSync = async () => { - reg.waitForReSyncCalls_.push(true); - try { - const synchronizer = await reg.syncTarget().synchronizer(); - await synchronizer.waitForSyncToFinish(); - await reg.scheduleSync(0); - } finally { - reg.waitForReSyncCalls_.pop(); - } -}; - -reg.scheduleSync = async (delay = null, syncOptions = null) => { - reg.schedSyncCalls_.push(true); - - try { - if (delay === null) delay = 1000 * 10; - if (syncOptions === null) syncOptions = {}; - - let promiseResolve = null; - const promise = new Promise((resolve) => { - promiseResolve = resolve; - }); - - if (reg.scheduleSyncId_) { - shim.clearTimeout(reg.scheduleSyncId_); - reg.scheduleSyncId_ = null; - } - - reg.logger().debug('Scheduling sync operation...', delay); - - if (Setting.value('env') === 'dev' && delay !== 0) { - reg.logger().info('Schedule sync DISABLED!!!'); - return; - } - - const timeoutCallback = async () => { - reg.timerCallbackCalls_.push(true); - try { - reg.scheduleSyncId_ = null; - reg.logger().info('Preparing scheduled sync'); - - const syncTargetId = Setting.value('sync.target'); - - if (!(await reg.syncTarget(syncTargetId).isAuthenticated())) { - reg.logger().info('Synchroniser is missing credentials - manual sync required to authenticate.'); - promiseResolve(); - return; - } - - try { - const sync = await reg.syncTarget(syncTargetId).synchronizer(); - - const contextKey = `sync.${syncTargetId}.context`; - let context = Setting.value(contextKey); - try { - context = context ? JSON.parse(context) : {}; - } catch (error) { - // Clearing the context is inefficient since it means all items are going to be re-downloaded - // however it won't result in duplicate items since the synchroniser is going to compare each - // item to the current state. - reg.logger().warn(`Could not parse JSON sync context ${contextKey}:`, context); - reg.logger().info('Clearing context and starting from scratch'); - context = null; - } - - try { - reg.logger().info('Starting scheduled sync'); - const options = Object.assign({}, syncOptions, { context: context }); - if (!options.saveContextHandler) { - options.saveContextHandler = newContext => { - Setting.setValue(contextKey, JSON.stringify(newContext)); - }; - } - const newContext = await sync.start(options); - Setting.setValue(contextKey, JSON.stringify(newContext)); - } catch (error) { - if (error.code == 'alreadyStarted') { - reg.logger().info(error.message); - } else { - promiseResolve(); - throw error; - } - } - } catch (error) { - reg.logger().info('Could not run background sync:'); - reg.logger().info(error); - } - reg.setupRecurrentSync(); - promiseResolve(); - - } finally { - reg.timerCallbackCalls_.pop(); - } - }; - - if (delay === 0) { - timeoutCallback(); - } else { - reg.scheduleSyncId_ = shim.setTimeout(timeoutCallback, delay); - } - return promise; - - } finally { - reg.schedSyncCalls_.pop(); - } -}; - -reg.setupRecurrentSync = () => { - reg.setupRecurrentCalls_.push(true); - - try { - if (reg.recurrentSyncId_) { - shim.clearInterval(reg.recurrentSyncId_); - reg.recurrentSyncId_ = null; - } - - if (!Setting.value('sync.interval')) { - reg.logger().debug('Recurrent sync is disabled'); - } else { - reg.logger().debug(`Setting up recurrent sync with interval ${Setting.value('sync.interval')}`); - - if (Setting.value('env') === 'dev') { - reg.logger().info('Recurrent sync operation DISABLED!!!'); - return; - } - - reg.recurrentSyncId_ = shim.setInterval(() => { - reg.logger().info('Running background sync on timer...'); - reg.scheduleSync(0); - }, 1000 * Setting.value('sync.interval')); - } - } finally { - reg.setupRecurrentCalls_.pop(); - } -}; - -reg.setDb = v => { - reg.db_ = v; -}; - -reg.db = () => { - return reg.db_; -}; - -reg.cancelTimers_ = () => { - if (this.recurrentSyncId_) { - shim.clearInterval(reg.recurrentSyncId_); - this.recurrentSyncId_ = null; - } - if (reg.scheduleSyncId_) { - shim.clearTimeout(reg.scheduleSyncId_); - reg.scheduleSyncId_ = null; - } -}; - -reg.cancelTimers = async () => { - reg.logger().info('Cancelling sync timers'); - reg.cancelTimers_(); - - return new Promise((resolve) => { - shim.setInterval(() => { - // ensure processing complete - if (!reg.setupRecurrentCalls_.length && !reg.schedSyncCalls_.length && !reg.timerCallbackCalls_.length && !reg.waitForReSyncCalls_.length) { - reg.cancelTimers_(); - resolve(); - } - }, 100); - }); -}; - -reg.syncCalls_ = []; -reg.schedSyncCalls_ = []; -reg.waitForReSyncCalls_ = []; -reg.setupRecurrentCalls_ = []; -reg.timerCallbackCalls_ = []; - -module.exports = { reg }; +class Registry { + constructor() { + this.syncTargets_ = {}; + this.logger_ = null; + this.schedSyncCalls_ = []; + this.waitForReSyncCalls_ = []; + this.setupRecurrentCalls_ = []; + this.timerCallbackCalls_ = []; + this.syncTarget = (syncTargetId = null) => { + if (syncTargetId === null) + syncTargetId = Setting_1.default.value('sync.target'); + if (this.syncTargets_[syncTargetId]) + return this.syncTargets_[syncTargetId]; + const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId); + if (!this.db()) + throw new Error('Cannot initialize sync without a db'); + const target = new SyncTargetClass(this.db()); + target.setLogger(this.logger()); + this.syncTargets_[syncTargetId] = target; + return target; + }; + // This can be used when some data has been modified and we want to make + // sure it gets synced. So we wait for the current sync operation to + // finish (if one is running), then we trigger a sync just after. + this.waitForSyncFinishedThenSync = () => __awaiter(this, void 0, void 0, function* () { + this.waitForReSyncCalls_.push(true); + try { + const synchronizer = yield this.syncTarget().synchronizer(); + yield synchronizer.waitForSyncToFinish(); + yield this.scheduleSync(0); + } + finally { + this.waitForReSyncCalls_.pop(); + } + }); + this.scheduleSync = (delay = null, syncOptions = null) => __awaiter(this, void 0, void 0, function* () { + this.schedSyncCalls_.push(true); + try { + if (delay === null) + delay = 1000 * 10; + if (syncOptions === null) + syncOptions = {}; + let promiseResolve = null; + const promise = new Promise((resolve) => { + promiseResolve = resolve; + }); + if (this.scheduleSyncId_) { + shim_1.default.clearTimeout(this.scheduleSyncId_); + this.scheduleSyncId_ = null; + } + this.logger().debug('Scheduling sync operation...', delay); + if (Setting_1.default.value('env') === 'dev' && delay !== 0) { + this.logger().info('Schedule sync DISABLED!!!'); + return; + } + const timeoutCallback = () => __awaiter(this, void 0, void 0, function* () { + this.timerCallbackCalls_.push(true); + try { + this.scheduleSyncId_ = null; + this.logger().info('Preparing scheduled sync'); + const syncTargetId = Setting_1.default.value('sync.target'); + if (!(yield this.syncTarget(syncTargetId).isAuthenticated())) { + this.logger().info('Synchroniser is missing credentials - manual sync required to authenticate.'); + promiseResolve(); + return; + } + try { + const sync = yield this.syncTarget(syncTargetId).synchronizer(); + const contextKey = `sync.${syncTargetId}.context`; + let context = Setting_1.default.value(contextKey); + try { + context = context ? JSON.parse(context) : {}; + } + catch (error) { + // Clearing the context is inefficient since it means all items are going to be re-downloaded + // however it won't result in duplicate items since the synchroniser is going to compare each + // item to the current state. + this.logger().warn(`Could not parse JSON sync context ${contextKey}:`, context); + this.logger().info('Clearing context and starting from scratch'); + context = null; + } + try { + this.logger().info('Starting scheduled sync'); + const options = Object.assign({}, syncOptions, { context: context }); + if (!options.saveContextHandler) { + options.saveContextHandler = (newContext) => { + Setting_1.default.setValue(contextKey, JSON.stringify(newContext)); + }; + } + const newContext = yield sync.start(options); + Setting_1.default.setValue(contextKey, JSON.stringify(newContext)); + } + catch (error) { + if (error.code == 'alreadyStarted') { + this.logger().info(error.message); + } + else { + promiseResolve(); + throw error; + } + } + } + catch (error) { + this.logger().info('Could not run background sync:'); + this.logger().info(error); + } + this.setupRecurrentSync(); + promiseResolve(); + } + finally { + this.timerCallbackCalls_.pop(); + } + }); + if (delay === 0) { + void timeoutCallback(); + } + else { + this.scheduleSyncId_ = shim_1.default.setTimeout(timeoutCallback, delay); + } + return promise; + } + finally { + this.schedSyncCalls_.pop(); + } + }); + this.setDb = (v) => { + this.db_ = v; + }; + this.cancelTimers = () => __awaiter(this, void 0, void 0, function* () { + this.logger().info('Cancelling sync timers'); + this.cancelTimers_(); + return new Promise((resolve) => { + shim_1.default.setInterval(() => { + // ensure processing complete + if (!this.setupRecurrentCalls_.length && !this.schedSyncCalls_.length && !this.timerCallbackCalls_.length && !this.waitForReSyncCalls_.length) { + this.cancelTimers_(); + resolve(null); + } + }, 100); + }); + }); + } + logger() { + if (!this.logger_) { + // console.warn('Calling logger before it is initialized'); + return new Logger_1.default(); + } + return this.logger_; + } + setLogger(l) { + this.logger_ = l; + } + setShowErrorMessageBoxHandler(v) { + this.showErrorMessageBoxHandler_ = v; + } + showErrorMessageBox(message) { + if (!this.showErrorMessageBoxHandler_) + return; + this.showErrorMessageBoxHandler_(message); + } + resetSyncTarget(syncTargetId = null) { + if (syncTargetId === null) + syncTargetId = Setting_1.default.value('sync.target'); + delete this.syncTargets_[syncTargetId]; + } + syncTargetNextcloud() { + return this.syncTarget(SyncTargetRegistry.nameToId('nextcloud')); + } + setupRecurrentSync() { + this.setupRecurrentCalls_.push(true); + try { + if (this.recurrentSyncId_) { + shim_1.default.clearInterval(this.recurrentSyncId_); + this.recurrentSyncId_ = null; + } + if (!Setting_1.default.value('sync.interval')) { + this.logger().debug('Recurrent sync is disabled'); + } + else { + this.logger().debug(`Setting up recurrent sync with interval ${Setting_1.default.value('sync.interval')}`); + if (Setting_1.default.value('env') === 'dev') { + this.logger().info('Recurrent sync operation DISABLED!!!'); + return; + } + this.recurrentSyncId_ = shim_1.default.setInterval(() => { + this.logger().info('Running background sync on timer...'); + void this.scheduleSync(0); + }, 1000 * Setting_1.default.value('sync.interval')); + } + } + finally { + this.setupRecurrentCalls_.pop(); + } + } + db() { + return this.db_; + } + cancelTimers_() { + if (this.recurrentSyncId_) { + shim_1.default.clearInterval(this.recurrentSyncId_); + this.recurrentSyncId_ = null; + } + if (this.scheduleSyncId_) { + shim_1.default.clearTimeout(this.scheduleSyncId_); + this.scheduleSyncId_ = null; + } + } +} +const reg = new Registry(); +exports.reg = reg; +//# sourceMappingURL=registry.js.map \ No newline at end of file diff --git a/packages/lib/registry.ts b/packages/lib/registry.ts new file mode 100644 index 0000000000..51fc9050bf --- /dev/null +++ b/packages/lib/registry.ts @@ -0,0 +1,241 @@ +import Logger from './Logger'; +import Setting from './models/Setting'; +import shim from './shim'; +const SyncTargetRegistry = require('./SyncTargetRegistry.js'); + +class Registry { + + private syncTargets_: any = {}; + private logger_: Logger = null; + private schedSyncCalls_: boolean[] = []; + private waitForReSyncCalls_: boolean[]= []; + private setupRecurrentCalls_: boolean[] = []; + private timerCallbackCalls_: boolean[] = []; + private showErrorMessageBoxHandler_: any; + private scheduleSyncId_: any; + private recurrentSyncId_: any; + private db_: any; + + logger() { + if (!this.logger_) { + // console.warn('Calling logger before it is initialized'); + return new Logger(); + } + + return this.logger_; + } + + setLogger(l: Logger) { + this.logger_ = l; + } + + setShowErrorMessageBoxHandler(v: any) { + this.showErrorMessageBoxHandler_ = v; + } + + showErrorMessageBox(message: string) { + if (!this.showErrorMessageBoxHandler_) return; + this.showErrorMessageBoxHandler_(message); + } + + resetSyncTarget(syncTargetId: number = null) { + if (syncTargetId === null) syncTargetId = Setting.value('sync.target'); + delete this.syncTargets_[syncTargetId]; + } + + syncTargetNextcloud() { + return this.syncTarget(SyncTargetRegistry.nameToId('nextcloud')); + } + + syncTarget = (syncTargetId: number = null) => { + if (syncTargetId === null) syncTargetId = Setting.value('sync.target'); + if (this.syncTargets_[syncTargetId]) return this.syncTargets_[syncTargetId]; + + const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId); + if (!this.db()) throw new Error('Cannot initialize sync without a db'); + + const target = new SyncTargetClass(this.db()); + target.setLogger(this.logger()); + this.syncTargets_[syncTargetId] = target; + return target; + }; + + // This can be used when some data has been modified and we want to make + // sure it gets synced. So we wait for the current sync operation to + // finish (if one is running), then we trigger a sync just after. + waitForSyncFinishedThenSync = async () => { + this.waitForReSyncCalls_.push(true); + try { + const synchronizer = await this.syncTarget().synchronizer(); + await synchronizer.waitForSyncToFinish(); + await this.scheduleSync(0); + } finally { + this.waitForReSyncCalls_.pop(); + } + }; + + scheduleSync = async (delay: number = null, syncOptions: any = null) => { + this.schedSyncCalls_.push(true); + + try { + if (delay === null) delay = 1000 * 10; + if (syncOptions === null) syncOptions = {}; + + let promiseResolve: Function = null; + const promise = new Promise((resolve) => { + promiseResolve = resolve; + }); + + if (this.scheduleSyncId_) { + shim.clearTimeout(this.scheduleSyncId_); + this.scheduleSyncId_ = null; + } + + this.logger().debug('Scheduling sync operation...', delay); + + if (Setting.value('env') === 'dev' && delay !== 0) { + this.logger().info('Schedule sync DISABLED!!!'); + return; + } + + const timeoutCallback = async () => { + this.timerCallbackCalls_.push(true); + try { + this.scheduleSyncId_ = null; + this.logger().info('Preparing scheduled sync'); + + const syncTargetId = Setting.value('sync.target'); + + if (!(await this.syncTarget(syncTargetId).isAuthenticated())) { + this.logger().info('Synchroniser is missing credentials - manual sync required to authenticate.'); + promiseResolve(); + return; + } + + try { + const sync = await this.syncTarget(syncTargetId).synchronizer(); + + const contextKey = `sync.${syncTargetId}.context`; + let context = Setting.value(contextKey); + try { + context = context ? JSON.parse(context) : {}; + } catch (error) { + // Clearing the context is inefficient since it means all items are going to be re-downloaded + // however it won't result in duplicate items since the synchroniser is going to compare each + // item to the current state. + this.logger().warn(`Could not parse JSON sync context ${contextKey}:`, context); + this.logger().info('Clearing context and starting from scratch'); + context = null; + } + + try { + this.logger().info('Starting scheduled sync'); + const options = Object.assign({}, syncOptions, { context: context }); + if (!options.saveContextHandler) { + options.saveContextHandler = (newContext: any) => { + Setting.setValue(contextKey, JSON.stringify(newContext)); + }; + } + const newContext = await sync.start(options); + Setting.setValue(contextKey, JSON.stringify(newContext)); + } catch (error) { + if (error.code == 'alreadyStarted') { + this.logger().info(error.message); + } else { + promiseResolve(); + throw error; + } + } + } catch (error) { + this.logger().info('Could not run background sync:'); + this.logger().info(error); + } + this.setupRecurrentSync(); + promiseResolve(); + + } finally { + this.timerCallbackCalls_.pop(); + } + }; + + if (delay === 0) { + void timeoutCallback(); + } else { + this.scheduleSyncId_ = shim.setTimeout(timeoutCallback, delay); + } + return promise; + + } finally { + this.schedSyncCalls_.pop(); + } + }; + + setupRecurrentSync() { + this.setupRecurrentCalls_.push(true); + + try { + if (this.recurrentSyncId_) { + shim.clearInterval(this.recurrentSyncId_); + this.recurrentSyncId_ = null; + } + + if (!Setting.value('sync.interval')) { + this.logger().debug('Recurrent sync is disabled'); + } else { + this.logger().debug(`Setting up recurrent sync with interval ${Setting.value('sync.interval')}`); + + if (Setting.value('env') === 'dev') { + this.logger().info('Recurrent sync operation DISABLED!!!'); + return; + } + + this.recurrentSyncId_ = shim.setInterval(() => { + this.logger().info('Running background sync on timer...'); + void this.scheduleSync(0); + }, 1000 * Setting.value('sync.interval')); + } + } finally { + this.setupRecurrentCalls_.pop(); + } + } + + setDb = (v: any) => { + this.db_ = v; + }; + + db() { + return this.db_; + } + + cancelTimers_() { + if (this.recurrentSyncId_) { + shim.clearInterval(this.recurrentSyncId_); + this.recurrentSyncId_ = null; + } + if (this.scheduleSyncId_) { + shim.clearTimeout(this.scheduleSyncId_); + this.scheduleSyncId_ = null; + } + } + + cancelTimers = async () => { + this.logger().info('Cancelling sync timers'); + this.cancelTimers_(); + + return new Promise((resolve) => { + shim.setInterval(() => { + // ensure processing complete + if (!this.setupRecurrentCalls_.length && !this.schedSyncCalls_.length && !this.timerCallbackCalls_.length && !this.waitForReSyncCalls_.length) { + this.cancelTimers_(); + resolve(null); + } + }, 100); + }); + }; + +} + +const reg = new Registry(); + +// eslint-disable-next-line import/prefer-default-export +export { reg }; diff --git a/packages/lib/services/ResourceService.ts b/packages/lib/services/ResourceService.ts index 6424ece509..97b4f4fd97 100644 --- a/packages/lib/services/ResourceService.ts +++ b/packages/lib/services/ResourceService.ts @@ -8,11 +8,13 @@ import Note from '../models/Note'; import Resource from '../models/Resource'; import SearchEngine from './searchengine/SearchEngine'; import ItemChangeUtils from './ItemChangeUtils'; +import time from '../time'; const { sprintf } = require('sprintf-js'); export default class ResourceService extends BaseService { public static isRunningInBackground_: boolean = false; + private isIndexing_: boolean = false; private maintenanceCalls_: boolean[] = []; private maintenanceTimer1_: any = null; @@ -21,76 +23,89 @@ export default class ResourceService extends BaseService { public async indexNoteResources() { this.logger().info('ResourceService::indexNoteResources: Start'); - await ItemChange.waitForAllSaved(); - - let foundNoteWithEncryption = false; - - while (true) { - const changes = await ItemChange.modelSelectAll(` - SELECT id, item_id, type - FROM item_changes - WHERE item_type = ? - AND id > ? - ORDER BY id ASC - LIMIT 10 - `, - [BaseModel.TYPE_NOTE, Setting.value('resourceService.lastProcessedChangeId')] - ); - - if (!changes.length) break; - - const noteIds = changes.map((a: any) => a.item_id); - const notes = await Note.modelSelectAll(`SELECT id, title, body, encryption_applied FROM notes WHERE id IN ("${noteIds.join('","')}")`); - - const noteById = (noteId: string) => { - for (let i = 0; i < notes.length; i++) { - if (notes[i].id === noteId) return notes[i]; - } - // The note may have been deleted since the change was recorded. For example in this case: - // - Note created (Some Change object is recorded) - // - Note is deleted - // - ResourceService indexer runs. - // In that case, there will be a change for the note, but the note will be gone. - return null; - }; - - for (let i = 0; i < changes.length; i++) { - const change = changes[i]; - - if (change.type === ItemChange.TYPE_CREATE || change.type === ItemChange.TYPE_UPDATE) { - const note = noteById(change.item_id); - - if (note) { - if (note.encryption_applied) { - // If we hit an encrypted note, abort processing for now. - // Note will eventually get decrypted and processing can resume then. - // This is a limitation of the change tracking system - we cannot skip a change - // and keep processing the rest since we only keep track of "lastProcessedChangeId". - foundNoteWithEncryption = true; - break; - } - - await this.setAssociatedResources(note.id, note.body); - } else { - this.logger().warn(`ResourceService::indexNoteResources: A change was recorded for a note that has been deleted: ${change.item_id}`); - } - } else if (change.type === ItemChange.TYPE_DELETE) { - await NoteResource.remove(change.item_id); - } else { - throw new Error(`Invalid change type: ${change.type}`); - } - - Setting.setValue('resourceService.lastProcessedChangeId', change.id); - } - - if (foundNoteWithEncryption) break; + if (this.isIndexing_) { + this.logger().info('ResourceService::indexNoteResources: Already indexing - waiting for it to finish'); + await time.waitTillCondition(() => !this.isIndexing_); + return; } - await Setting.saveAll(); + this.isIndexing_ = true; - await NoteResource.addOrphanedResources(); + try { + await ItemChange.waitForAllSaved(); - await ItemChangeUtils.deleteProcessedChanges(); + let foundNoteWithEncryption = false; + + while (true) { + const changes = await ItemChange.modelSelectAll(` + SELECT id, item_id, type + FROM item_changes + WHERE item_type = ? + AND id > ? + ORDER BY id ASC + LIMIT 10 + `, [BaseModel.TYPE_NOTE, Setting.value('resourceService.lastProcessedChangeId')] + ); + + if (!changes.length) break; + + const noteIds = changes.map((a: any) => a.item_id); + const notes = await Note.modelSelectAll(`SELECT id, title, body, encryption_applied FROM notes WHERE id IN ("${noteIds.join('","')}")`); + + const noteById = (noteId: string) => { + for (let i = 0; i < notes.length; i++) { + if (notes[i].id === noteId) return notes[i]; + } + // The note may have been deleted since the change was recorded. For example in this case: + // - Note created (Some Change object is recorded) + // - Note is deleted + // - ResourceService indexer runs. + // In that case, there will be a change for the note, but the note will be gone. + return null; + }; + + for (let i = 0; i < changes.length; i++) { + const change = changes[i]; + + if (change.type === ItemChange.TYPE_CREATE || change.type === ItemChange.TYPE_UPDATE) { + const note = noteById(change.item_id); + + if (note) { + if (note.encryption_applied) { + // If we hit an encrypted note, abort processing for now. + // Note will eventually get decrypted and processing can resume then. + // This is a limitation of the change tracking system - we cannot skip a change + // and keep processing the rest since we only keep track of "lastProcessedChangeId". + foundNoteWithEncryption = true; + break; + } + + await this.setAssociatedResources(note.id, note.body); + } else { + this.logger().warn(`ResourceService::indexNoteResources: A change was recorded for a note that has been deleted: ${change.item_id}`); + } + } else if (change.type === ItemChange.TYPE_DELETE) { + await NoteResource.remove(change.item_id); + } else { + throw new Error(`Invalid change type: ${change.type}`); + } + + Setting.setValue('resourceService.lastProcessedChangeId', change.id); + } + + if (foundNoteWithEncryption) break; + } + + await Setting.saveAll(); + + await NoteResource.addOrphanedResources(); + + await ItemChangeUtils.deleteProcessedChanges(); + } catch (error) { + this.logger().error('ResourceService::indexNoteResources:', error); + } + + this.isIndexing_ = false; this.logger().info('ResourceService::indexNoteResources: Completed'); } @@ -176,7 +191,7 @@ export default class ResourceService extends BaseService { const iid = shim.setInterval(() => { if (!this.maintenanceCalls_.length) { shim.clearInterval(iid); - resolve(); + resolve(null); } }, 100); }); diff --git a/packages/lib/services/database/types.ts b/packages/lib/services/database/types.ts index b06d03f2ec..9853f513ef 100644 --- a/packages/lib/services/database/types.ts +++ b/packages/lib/services/database/types.ts @@ -6,6 +6,7 @@ export interface AlarmEntity { 'id'?: number | null; 'note_id'?: string; 'trigger_time'?: number; + 'type_'?: number; } export interface DeletedItemEntity { 'id'?: number | null; @@ -13,6 +14,7 @@ export interface DeletedItemEntity { 'item_id'?: string; 'deleted_time'?: number; 'sync_target'?: number; + 'type_'?: number; } export interface FolderEntity { 'id'?: string | null; @@ -25,6 +27,7 @@ export interface FolderEntity { 'encryption_applied'?: number; 'parent_id'?: string; 'is_shared'?: number; + 'type_'?: number; } export interface ItemChangeEntity { 'id'?: number | null; @@ -34,6 +37,7 @@ export interface ItemChangeEntity { 'created_time'?: number; 'source'?: number; 'before_change_item'?: string; + 'type_'?: number; } export interface KeyValueEntity { 'id'?: number | null; @@ -41,6 +45,7 @@ export interface KeyValueEntity { 'value'?: string; 'type'?: number; 'updated_time'?: number; + 'type_'?: number; } export interface MasterKeyEntity { 'id'?: string | null; @@ -50,12 +55,14 @@ export interface MasterKeyEntity { 'encryption_method'?: number; 'checksum'?: string; 'content'?: string; + 'type_'?: number; } export interface MigrationEntity { 'id'?: number | null; 'number'?: number; 'updated_time'?: number; 'created_time'?: number; + 'type_'?: number; } export interface NoteResourceEntity { 'id'?: number | null; @@ -63,6 +70,7 @@ export interface NoteResourceEntity { 'resource_id'?: string; 'is_associated'?: number; 'last_seen_time'?: number; + 'type_'?: number; } export interface NoteTagEntity { 'id'?: string | null; @@ -75,6 +83,7 @@ export interface NoteTagEntity { 'encryption_cipher_text'?: string; 'encryption_applied'?: number; 'is_shared'?: number; + 'type_'?: number; } export interface NoteEntity { 'id'?: string | null; @@ -102,6 +111,7 @@ export interface NoteEntity { 'encryption_applied'?: number; 'markup_language'?: number; 'is_shared'?: number; + 'type_'?: number; } export interface NotesNormalizedEntity { 'id'?: string; @@ -116,12 +126,14 @@ export interface NotesNormalizedEntity { 'longitude'?: number; 'altitude'?: number; 'source_url'?: string; + 'type_'?: number; } export interface ResourceLocalStateEntity { 'id'?: number | null; 'resource_id'?: string; 'fetch_status'?: number; 'fetch_error'?: string; + 'type_'?: number; } export interface ResourceEntity { 'id'?: string | null; @@ -138,12 +150,14 @@ export interface ResourceEntity { 'encryption_blob_encrypted'?: number; 'size'?: number; 'is_shared'?: number; + 'type_'?: number; } export interface ResourcesToDownloadEntity { 'id'?: number | null; 'resource_id'?: string; 'updated_time'?: number; 'created_time'?: number; + 'type_'?: number; } export interface RevisionEntity { 'id'?: string | null; @@ -158,10 +172,12 @@ export interface RevisionEntity { 'encryption_applied'?: number; 'updated_time'?: number; 'created_time'?: number; + 'type_'?: number; } export interface SettingEntity { 'key'?: string | null; 'value'?: string | null; + 'type_'?: number; } export interface SyncItemEntity { 'id'?: number | null; @@ -173,6 +189,7 @@ export interface SyncItemEntity { 'sync_disabled_reason'?: string; 'force_sync'?: number; 'item_location'?: number; + 'type_'?: number; } export interface TableFieldEntity { 'id'?: number | null; @@ -180,6 +197,7 @@ export interface TableFieldEntity { 'field_name'?: string; 'field_type'?: number; 'field_default'?: string | null; + 'type_'?: number; } export interface TagEntity { 'id'?: string | null; @@ -192,6 +210,7 @@ export interface TagEntity { 'encryption_applied'?: number; 'is_shared'?: number; 'parent_id'?: string; + 'type_'?: number; } export interface TagsWithNoteCountEntity { 'id'?: string | null; @@ -199,8 +218,10 @@ export interface TagsWithNoteCountEntity { 'created_time'?: number | null; 'updated_time'?: number | null; 'note_count'?: any | null; + 'type_'?: number; } export interface VersionEntity { 'version'?: number; 'table_fields_version'?: number; + 'type_'?: number; } diff --git a/packages/lib/services/rest/routes/notes.ts b/packages/lib/services/rest/routes/notes.ts index 9453a9115e..c9ffcdc25f 100644 --- a/packages/lib/services/rest/routes/notes.ts +++ b/packages/lib/services/rest/routes/notes.ts @@ -10,8 +10,8 @@ import { RequestMethod, Request } from '../Api'; import markdownUtils from '../../../markdownUtils'; import collectionToPaginatedResults from '../utils/collectionToPaginatedResults'; -const { reg } = require('../../../registry.js'); -const { Database } = require('../../../database.js'); +import { reg } from '../../../registry'; +import Database from '../../../database'; import Folder from '../../../models/Folder'; import Note from '../../../models/Note'; import Tag from '../../../models/Tag'; diff --git a/packages/lib/services/synchronizer/gui/useSyncTargetUpgrade.ts b/packages/lib/services/synchronizer/gui/useSyncTargetUpgrade.ts index ffd678034d..0e3fe9c509 100644 --- a/packages/lib/services/synchronizer/gui/useSyncTargetUpgrade.ts +++ b/packages/lib/services/synchronizer/gui/useSyncTargetUpgrade.ts @@ -2,7 +2,7 @@ import shim from '../../../shim'; import MigrationHandler from '../MigrationHandler'; const { useEffect, useState } = shim.react(); import Setting from '../../../models/Setting'; -const { reg } = require('../../../registry'); +import { reg } from '../../../registry'; export interface SyncTargetUpgradeResult { done: boolean; diff --git a/packages/lib/shim.ts b/packages/lib/shim.ts index b5ae64793d..80301ee4d1 100644 --- a/packages/lib/shim.ts +++ b/packages/lib/shim.ts @@ -21,6 +21,14 @@ let react_: any = null; const shim = { Geolocation: null as any, + msleep_: (ms: number) => { + return new Promise((resolve: Function) => { + shim.setTimeout(() => { + resolve(null); + }, ms); + }); + }, + isNode: () => { if (typeof process === 'undefined') return false; if (shim.isElectron()) return true; @@ -140,8 +148,6 @@ const shim = { }, fetchWithRetry: async function(fetchFn: Function, options: any = null) { - const time = require('./time'); - if (!options) options = {}; if (!options.timeout) options.timeout = 1000 * 120; // ms if (!('maxRetry' in options)) options.maxRetry = shim.fetchMaxRetry_; @@ -155,7 +161,7 @@ const shim = { if (shim.fetchRequestCanBeRetried(error)) { retryCount++; if (retryCount > options.maxRetry) throw error; - await time.sleep(retryCount * 3); + await shim.msleep_(retryCount * 3000); } else { throw error; } diff --git a/packages/lib/theme.ts b/packages/lib/theme.ts index 03d607f4c9..7a814d9ddf 100644 --- a/packages/lib/theme.ts +++ b/packages/lib/theme.ts @@ -368,10 +368,9 @@ const themeCache_: any = {}; function themeStyle(themeId: number) { if (!themeId) throw new Error('Theme must be specified'); - const zoomRatio = 1; // Setting.value('style.zoom') / 100; - const editorFontSize = Setting.value('style.editor.fontSize'); + const zoomRatio = 1; - const cacheKey = [themeId, zoomRatio, editorFontSize].join('-'); + const cacheKey = themeId; if (themeCache_[cacheKey]) return themeCache_[cacheKey]; // Font size are not theme specific, but they must be referenced @@ -380,8 +379,6 @@ function themeStyle(themeId: number) { const fontSizes: any = { fontSize: Math.round(12 * zoomRatio), toolbarIconSize: 18, - editorFontSize: editorFontSize, - textAreaLineHeight: Math.round(globalStyle.textAreaLineHeight * editorFontSize / 12), }; fontSizes.noteViewerFontSize = Math.round(fontSizes.fontSize * 1.25); diff --git a/packages/lib/time.ts b/packages/lib/time.ts index 2678329e86..95c5edd089 100644 --- a/packages/lib/time.ts +++ b/packages/lib/time.ts @@ -1,66 +1,68 @@ import shim from './shim'; const moment = require('moment'); +type ConditionHandler = ()=> boolean; + class Time { private dateFormat_: string = 'DD/MM/YYYY'; private timeFormat_: string = 'HH:mm'; private locale_: string = 'en-us'; - locale() { + public locale() { return this.locale_; } - setLocale(v: string) { + public setLocale(v: string) { moment.locale(v); this.locale_ = v; } - dateFormat() { + public dateFormat() { return this.dateFormat_; } - setDateFormat(v: string) { + public setDateFormat(v: string) { this.dateFormat_ = v; } - timeFormat() { + public timeFormat() { return this.timeFormat_; } - setTimeFormat(v: string) { + public setTimeFormat(v: string) { this.timeFormat_ = v; } - use24HourFormat() { + public use24HourFormat() { return this.timeFormat() ? this.timeFormat().includes('HH') : true; } - formatDateToLocal(date: Date, format: string = null) { + public formatDateToLocal(date: Date, format: string = null) { return this.formatMsToLocal(date.getTime(), format); } - dateTimeFormat() { + public dateTimeFormat() { return `${this.dateFormat()} ${this.timeFormat()}`; } - unix() { + public unix() { return Math.floor(Date.now() / 1000); } - unixMs() { + public unixMs() { return Date.now(); } - unixMsToObject(ms: number) { + public unixMsToObject(ms: number) { return new Date(ms); } - unixMsToS(ms: number) { + public unixMsToS(ms: number) { return Math.floor(ms / 1000); } - unixMsToIso(ms: number) { + public unixMsToIso(ms: number) { return ( `${moment .unix(ms / 1000) @@ -69,7 +71,7 @@ class Time { ); } - unixMsToIsoSec(ms: number) { + public unixMsToIsoSec(ms: number) { return ( `${moment .unix(ms / 1000) @@ -78,20 +80,20 @@ class Time { ); } - unixMsToLocalDateTime(ms: number) { + public unixMsToLocalDateTime(ms: number) { return moment.unix(ms / 1000).format('DD/MM/YYYY HH:mm'); } - unixMsToLocalHms(ms: number) { + public unixMsToLocalHms(ms: number) { return moment.unix(ms / 1000).format('HH:mm:ss'); } - formatMsToLocal(ms: number, format: string = null) { + public formatMsToLocal(ms: number, format: string = null) { if (format === null) format = this.dateTimeFormat(); return moment(ms).format(format); } - formatLocalToMs(localDateTime: any, format: string = null) { + public formatLocalToMs(localDateTime: any, format: string = null) { if (format === null) format = this.dateTimeFormat(); const m = moment(localDateTime, format); if (m.isValid()) return m.toDate().getTime(); @@ -99,7 +101,7 @@ class Time { } // Mostly used as a utility function for the DateTime Electron component - anythingToDateTime(o: any, defaultValue: Date = null) { + public anythingToDateTime(o: any, defaultValue: Date = null) { if (o && o.toDate) return o.toDate(); if (!o) return defaultValue; let m = moment(o, time.dateTimeFormat()); @@ -108,7 +110,7 @@ class Time { return m.isValid() ? m.toDate() : defaultValue; } - msleep(ms: number) { + public msleep(ms: number) { return new Promise((resolve: Function) => { shim.setTimeout(() => { resolve(); @@ -116,20 +118,33 @@ class Time { }); } - sleep(seconds: number) { + public sleep(seconds: number) { return this.msleep(seconds * 1000); } - goBackInTime(startDate: any, n: number, period: any) { + public goBackInTime(startDate: any, n: number, period: any) { // period is a string (eg. "day", "week", "month", "year" ), n is an integer return moment(startDate).startOf(period).subtract(n, period).format('x'); } - goForwardInTime(startDate: any, n: number, period: any) { + public goForwardInTime(startDate: any, n: number, period: any) { return moment(startDate).startOf(period).add(n, period).format('x'); } + public async waitTillCondition(condition: ConditionHandler) { + if (condition()) return; + + return new Promise(resolve => { + const iid = setInterval(() => { + if (condition()) { + clearInterval(iid); + resolve(null); + } + }, 1000); + }); + } + } const time = new Time(); diff --git a/packages/lib/versionInfo.ts b/packages/lib/versionInfo.ts index 45362750dc..65c36d8dd8 100644 --- a/packages/lib/versionInfo.ts +++ b/packages/lib/versionInfo.ts @@ -1,6 +1,6 @@ import { _ } from './locale'; import Setting from './models/Setting'; -const { reg } = require('./registry.js'); +import { reg } from './registry'; export default function versionInfo(packageInfo: any) { const p = packageInfo; diff --git a/packages/plugin-repo-cli/package-lock.json b/packages/plugin-repo-cli/package-lock.json index 79b4f3dd38..4e23663369 100644 --- a/packages/plugin-repo-cli/package-lock.json +++ b/packages/plugin-repo-cli/package-lock.json @@ -402,11 +402,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "@braintree/sanitize-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-3.1.0.tgz", - "integrity": "sha512-GcIY79elgB+azP74j8vqkiXz8xLFfIzbQJdlwOPisgbKT00tviJQuEghOXSMVxJ00HoYJbGswr4kcllUc4xCcg==" - }, "@cnakazawa/watch": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", @@ -628,462 +623,6 @@ "chalk": "^4.0.0" } }, - "@joplin/fork-htmlparser2": { - "version": "4.1.21", - "resolved": "https://registry.npmjs.org/@joplin/fork-htmlparser2/-/fork-htmlparser2-4.1.21.tgz", - "integrity": "sha512-1+3/llkmSFF62wCj+UPeHDO8QevQjEm8uGSq7SltHVARZgWuvE3s4U/sKOoWCuqs5u34QHC3DcJ2ZlJRTwgYdg==", - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^3.0.0", - "domutils": "^2.0.0", - "entities": "^2.0.0" - } - }, - "@joplin/fork-sax": { - "version": "1.2.25", - "resolved": "https://registry.npmjs.org/@joplin/fork-sax/-/fork-sax-1.2.25.tgz", - "integrity": "sha512-m5ZdP1JX0GmTaNz3zhUxjiGdtaDtPVqpY2+h3e5mh2WLwZJmT9TCKp7upB1+L+fOLgjI6em7xYHpvIL/dml+5Q==" - }, - "@joplin/lib": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@joplin/lib/-/lib-1.7.4.tgz", - "integrity": "sha512-TIPzbT6ZZVl5R8bCncxSP1EXS+EOYj8bWJuFrB/R/U7MCI77J3zK8CFMRT73C/3sYcTLFvpzz4E0DKt8Cf8GSw==", - "requires": { - "@joplin/fork-htmlparser2": "^4.1.21", - "@joplin/fork-sax": "^1.2.25", - "@joplin/renderer": "^1.7.4", - "@joplin/turndown": "^4.0.43", - "@joplin/turndown-plugin-gfm": "^1.0.25", - "async-mutex": "^0.1.3", - "aws-sdk": "^2.588.0", - "base-64": "^0.1.0", - "base64-stream": "^1.0.0", - "builtin-modules": "^3.1.0", - "chokidar": "^3.4.3", - "color": "3.1.2", - "compare-versions": "^3.6.0", - "diff-match-patch": "^1.0.4", - "es6-promise-pool": "^2.5.0", - "file-uri-to-path": "^1.0.0", - "follow-redirects": "^1.2.4", - "form-data": "^2.1.4", - "fs-extra": "^5.0.0", - "html-entities": "^1.2.1", - "html-minifier": "^3.5.15", - "image-data-uri": "^2.0.0", - "image-type": "^3.0.0", - "immer": "^7.0.14", - "levenshtein": "^1.0.5", - "lodash": "^4.17.20", - "markdown-it": "^10.0.0", - "md5": "^2.2.1", - "md5-file": "^4.0.0", - "moment": "^2.29.1", - "multiparty": "^4.2.1", - "mustache": "^4.0.1", - "nanoid": "^3.1.12", - "node-fetch": "^1.7.1", - "node-notifier": "^8.0.0", - "node-persist": "^2.1.0", - "promise": "^7.1.1", - "query-string": "4.3.4", - "re-reselect": "^4.0.0", - "read-chunk": "^2.1.0", - "redux": "^3.7.2", - "relative": "^3.0.2", - "reselect": "^4.0.0", - "server-destroy": "^1.0.1", - "sprintf-js": "^1.1.2", - "sqlite3": "^5.0.0", - "string-padding": "^1.0.2", - "string-to-stream": "^1.1.0", - "tar": "^4.4.10", - "tcp-port-used": "^0.1.2", - "uglifycss": "0.0.29", - "url-parse": "^1.4.7", - "uslug": "git+https://github.com/laurent22/uslug.git#emoji-support", - "uuid": "^3.0.1", - "valid-url": "^1.0.9", - "word-wrap": "^1.2.3", - "xml2js": "^0.4.19" - }, - "dependencies": { - "fs-extra": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", - "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==", - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" - } - } - }, - "@joplin/renderer": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@joplin/renderer/-/renderer-1.7.4.tgz", - "integrity": "sha512-ZbQGjpAXYNZehvi6p2XKF4BZUrhd+xigSmaF1JJ1+8txqECSBB90GUrxZczDN4UyNM+6v1TSBIlalE5VgFnhOQ==", - "requires": { - "@joplin/fork-htmlparser2": "^4.1.21", - "font-awesome-filetypes": "^2.1.0", - "fs-extra": "^8.1.0", - "highlight.js": "^10.2.1", - "html-entities": "^1.2.1", - "json-stringify-safe": "^5.0.1", - "katex": "^0.12.0", - "markdown-it": "^10.0.0", - "markdown-it-abbr": "^1.0.4", - "markdown-it-anchor": "^5.2.5", - "markdown-it-deflist": "^2.0.3", - "markdown-it-emoji": "^1.4.0", - "markdown-it-expand-tabs": "^1.0.13", - "markdown-it-footnote": "^3.0.2", - "markdown-it-ins": "^3.0.0", - "markdown-it-mark": "^3.0.0", - "markdown-it-multimd-table": "^4.0.1", - "markdown-it-sub": "^1.0.0", - "markdown-it-sup": "^1.0.0", - "markdown-it-toc-done-right": "^4.1.0", - "md5": "^2.2.1", - "mermaid": "^8.8.4", - "uslug": "git+https://github.com/laurent22/uslug.git#emoji-support" - }, - "dependencies": { - "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" - } - } - }, - "@joplin/tools": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@joplin/tools/-/tools-1.7.4.tgz", - "integrity": "sha512-L7kf4hHWRpZFxkh/ZSb5iRlXB6weBEe8J1B27FPQBkO4zOXPBqFPd3sKY+PKmbm0UJ9lcGhwVVk3NZSYVSg2mw==", - "requires": { - "@joplin/lib": "^1.7.4", - "execa": "^4.1.0", - "fs-extra": "^4.0.3", - "gettext-parser": "^1.3.0", - "glob": "^7.1.6", - "markdown-it": "^8.4.1", - "md5-file": "^4.0.0", - "moment": "^2.24.0", - "mustache": "^2.3.0", - "node-fetch": "^1.7.3", - "relative": "^3.0.2", - "request": "^2.88.0", - "sharp": "^0.25.2", - "source-map-support": "^0.5.19", - "uri-template": "^1.0.1", - "yargs": "^16.0.3" - }, - "dependencies": { - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" - }, - "execa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", - "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", - "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - } - }, - "fs-extra": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", - "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "requires": { - "pump": "^3.0.0" - } - }, - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "markdown-it": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", - "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", - "requires": { - "argparse": "^1.0.7", - "entities": "~1.1.1", - "linkify-it": "^2.0.0", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - } - }, - "mustache": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-2.3.2.tgz", - "integrity": "sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ==" - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "requires": { - "path-key": "^3.0.0" - } - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "@joplin/turndown": { - "version": "4.0.43", - "resolved": "https://registry.npmjs.org/@joplin/turndown/-/turndown-4.0.43.tgz", - "integrity": "sha512-ZDGADbHVRPqeNrxvLTQ9JjmuChu40AyUPOA4N8zC8NcNzZQl5ZtKxZinMOOCGm8TXNNm7S987MYtnHszogCwtg==", - "requires": { - "css": "^2.2.4", - "html-entities": "^1.2.1", - "jsdom": "^15.2.1" - }, - "dependencies": { - "acorn-globals": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", - "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", - "requires": { - "acorn": "^6.0.1", - "acorn-walk": "^6.0.1" - }, - "dependencies": { - "acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==" - } - } - }, - "acorn-walk": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", - "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==" - }, - "data-urls": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", - "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", - "requires": { - "abab": "^2.0.0", - "whatwg-mimetype": "^2.2.0", - "whatwg-url": "^7.0.0" - } - }, - "domexception": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", - "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", - "requires": { - "webidl-conversions": "^4.0.2" - } - }, - "html-encoding-sniffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", - "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", - "requires": { - "whatwg-encoding": "^1.0.1" - } - }, - "jsdom": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", - "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", - "requires": { - "abab": "^2.0.0", - "acorn": "^7.1.0", - "acorn-globals": "^4.3.2", - "array-equal": "^1.0.0", - "cssom": "^0.4.1", - "cssstyle": "^2.0.0", - "data-urls": "^1.1.0", - "domexception": "^1.0.1", - "escodegen": "^1.11.1", - "html-encoding-sniffer": "^1.0.2", - "nwsapi": "^2.2.0", - "parse5": "5.1.0", - "pn": "^1.1.0", - "request": "^2.88.0", - "request-promise-native": "^1.0.7", - "saxes": "^3.1.9", - "symbol-tree": "^3.2.2", - "tough-cookie": "^3.0.1", - "w3c-hr-time": "^1.0.1", - "w3c-xmlserializer": "^1.1.2", - "webidl-conversions": "^4.0.2", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^7.0.0", - "ws": "^7.0.0", - "xml-name-validator": "^3.0.0" - } - }, - "parse5": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", - "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==" - }, - "saxes": { - "version": "3.1.11", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", - "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", - "requires": { - "xmlchars": "^2.1.1" - } - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "requires": { - "punycode": "^2.1.0" - } - }, - "w3c-xmlserializer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz", - "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==", - "requires": { - "domexception": "^1.0.1", - "webidl-conversions": "^4.0.2", - "xml-name-validator": "^3.0.0" - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" - }, - "whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - } - } - }, - "@joplin/turndown-plugin-gfm": { - "version": "1.0.25", - "resolved": "https://registry.npmjs.org/@joplin/turndown-plugin-gfm/-/turndown-plugin-gfm-1.0.25.tgz", - "integrity": "sha512-ycWQ9a88tPH04sNfgZGsU5SuXWlOfEjVv2MLkF2D86ROyNIh7hxQe5sYvQTybhdUT2BkhLcRMm/qdp5Y/Cn67A==" - }, "@sinonjs/commons": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.2.tgz", @@ -1237,17 +776,14 @@ "abab": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", - "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==" - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", + "dev": true }, "acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true }, "acorn-globals": { "version": "6.0.0", @@ -1269,6 +805,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1276,21 +813,6 @@ "uri-js": "^4.2.2" } }, - "ansi-escape-sequences": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-escape-sequences/-/ansi-escape-sequences-4.1.0.tgz", - "integrity": "sha512-dzW9kHxH011uBsidTXd14JXgzye/YLb2LzeKZ4bsgl/Knwx8AtbSFkkGxagdNOoh0DlqHCmfiEjWKBaqjOanVw==", - "requires": { - "array-back": "^3.0.1" - }, - "dependencies": { - "array-back": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", - "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==" - } - } - }, "ansi-escapes": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", @@ -1325,29 +847,17 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, "requires": { "sprintf-js": "~1.0.2" } @@ -1370,34 +880,17 @@ "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", "dev": true }, - "array-back": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", - "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", - "requires": { - "typical": "^2.6.1" - } - }, - "array-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", - "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=" - }, "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" - }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, "requires": { "safer-buffer": "~2.1.0" } @@ -1405,7 +898,8 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true }, "assign-symbols": { "version": "1.0.0", @@ -1413,15 +907,11 @@ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", "dev": true }, - "async-mutex": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.1.4.tgz", - "integrity": "sha512-zVWTmAnxxHaeB2B1te84oecI8zTDJ/8G49aVBblRX6be0oq6pAybNcUSxwfgVOmOjSCvN4aYZAqwtyNI8e1YGw==" - }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true }, "at-least-node": { "version": "1.0.0", @@ -1431,49 +921,20 @@ "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" - }, - "aws-sdk": { - "version": "2.832.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.832.0.tgz", - "integrity": "sha512-LHoqfaSEvNJxZlVjofwcTR1ldEfoOvJKhkz4IeuHIup0QiA1uNWatEWLg0zRL0lmv9IWTERXJ3heqyM0fsuWyg==", - "requires": { - "buffer": "4.9.2", - "events": "1.1.1", - "ieee754": "1.1.13", - "jmespath": "0.15.0", - "querystring": "0.2.0", - "sax": "1.2.1", - "url": "0.10.3", - "uuid": "3.3.2", - "xml2js": "0.4.19" - }, - "dependencies": { - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - }, - "xml2js": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" - } - } - } + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true }, "aws4": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "dev": true }, "babel-jest": { "version": "26.6.3", @@ -1549,7 +1010,8 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true }, "base": { "version": "0.11.2", @@ -1606,78 +1068,20 @@ } } }, - "base-64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", - "integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs=" - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "base64-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/base64-stream/-/base64-stream-1.0.0.tgz", - "integrity": "sha512-BQQZftaO48FcE1Kof9CmXMFaAdqkcNorgc8CxesZv9nMbbTF1EFyQe89UOuh//QMmdtfUDXyO8rgUalemL5ODA==" - }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, "requires": { "tweetnacl": "^0.14.3" } }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" - }, - "bl": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", - "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - }, - "dependencies": { - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "optional": true, - "requires": { - "inherits": "~2.0.0" - } - }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1687,6 +1091,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, "requires": { "fill-range": "^7.0.1" } @@ -1694,7 +1099,8 @@ "browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", + "dev": true }, "bser": { "version": "2.1.1", @@ -1705,25 +1111,11 @@ "node-int64": "^0.4.0" } }, - "buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" - }, - "builtin-modules": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", - "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==" + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true }, "cache-base": { "version": "1.0.1", @@ -1748,15 +1140,6 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, - "camel-case": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", - "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", - "requires": { - "no-case": "^2.2.0", - "upper-case": "^1.1.1" - } - }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -1775,7 +1158,8 @@ "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true }, "chalk": { "version": "4.1.0", @@ -1793,31 +1177,6 @@ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true }, - "charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" - }, - "chokidar": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", - "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.3.1", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.5.0" - } - }, - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, "ci-info": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", @@ -1853,62 +1212,6 @@ } } }, - "clean-css": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", - "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", - "requires": { - "source-map": "~0.6.0" - } - }, - "cliss": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/cliss/-/cliss-0.0.2.tgz", - "integrity": "sha512-6rj9pgdukjT994Md13JCUAgTk91abAKrygL9sAvmHY4F6AKMOV8ccGaxhUUfcBuyg3sundWnn3JE0Mc9W6ZYqw==", - "requires": { - "command-line-usage": "^4.0.1", - "deepmerge": "^2.0.0", - "get-stdin": "^5.0.1", - "inspect-parameters-declaration": "0.0.9", - "object-to-arguments": "0.0.8", - "pipe-functions": "^1.3.0", - "strip-ansi": "^4.0.0", - "yargs-parser": "^7.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" - }, - "deepmerge": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", - "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==" - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "yargs-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", - "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", - "requires": { - "camelcase": "^4.1.0" - } - } - } - }, "cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -1925,11 +1228,6 @@ "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", "dev": true }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, "collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -1946,30 +1244,6 @@ "object-visit": "^1.0.0" } }, - "color": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/color/-/color-3.1.2.tgz", - "integrity": "sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==", - "requires": { - "color-convert": "^1.9.1", - "color-string": "^1.5.2" - }, - "dependencies": { - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - } - } - }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1983,44 +1257,15 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "color-string": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz", - "integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==", - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "requires": { "delayed-stream": "~1.0.0" } }, - "command-line-usage": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-4.1.0.tgz", - "integrity": "sha512-MxS8Ad995KpdAC0Jopo/ovGIroV/m0KHwzKfXxKag6FHOkGsH8/lv5yjgablcRxCJJC0oJeUMuO/gmaq+Wq46g==", - "requires": { - "ansi-escape-sequences": "^4.0.0", - "array-back": "^2.0.0", - "table-layout": "^0.4.2", - "typical": "^2.6.1" - } - }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "compare-versions": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", - "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==" - }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -2030,12 +1275,8 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "convert-source-map": { "version": "1.7.0", @@ -2055,7 +1296,8 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true }, "cross-spawn": { "version": "6.0.5", @@ -2070,36 +1312,17 @@ "which": "^1.2.9" } }, - "crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" - }, - "css": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz", - "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==", - "requires": { - "inherits": "^2.0.3", - "source-map": "^0.6.1", - "source-map-resolve": "^0.5.2", - "urix": "^0.1.0" - } - }, - "css-b64-images": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/css-b64-images/-/css-b64-images-0.2.5.tgz", - "integrity": "sha1-QgBdgyBLK0pdk7axpWRBM7WSegI=" - }, "cssom": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==" + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", + "dev": true }, "cssstyle": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, "requires": { "cssom": "~0.3.6" }, @@ -2107,298 +1330,16 @@ "cssom": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true } } }, - "d3": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/d3/-/d3-5.16.0.tgz", - "integrity": "sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==", - "requires": { - "d3-array": "1", - "d3-axis": "1", - "d3-brush": "1", - "d3-chord": "1", - "d3-collection": "1", - "d3-color": "1", - "d3-contour": "1", - "d3-dispatch": "1", - "d3-drag": "1", - "d3-dsv": "1", - "d3-ease": "1", - "d3-fetch": "1", - "d3-force": "1", - "d3-format": "1", - "d3-geo": "1", - "d3-hierarchy": "1", - "d3-interpolate": "1", - "d3-path": "1", - "d3-polygon": "1", - "d3-quadtree": "1", - "d3-random": "1", - "d3-scale": "2", - "d3-scale-chromatic": "1", - "d3-selection": "1", - "d3-shape": "1", - "d3-time": "1", - "d3-time-format": "2", - "d3-timer": "1", - "d3-transition": "1", - "d3-voronoi": "1", - "d3-zoom": "1" - } - }, - "d3-array": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", - "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" - }, - "d3-axis": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz", - "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==" - }, - "d3-brush": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.6.tgz", - "integrity": "sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA==", - "requires": { - "d3-dispatch": "1", - "d3-drag": "1", - "d3-interpolate": "1", - "d3-selection": "1", - "d3-transition": "1" - } - }, - "d3-chord": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz", - "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", - "requires": { - "d3-array": "1", - "d3-path": "1" - } - }, - "d3-collection": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", - "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" - }, - "d3-color": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz", - "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==" - }, - "d3-contour": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz", - "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", - "requires": { - "d3-array": "^1.1.1" - } - }, - "d3-dispatch": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", - "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==" - }, - "d3-drag": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.5.tgz", - "integrity": "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==", - "requires": { - "d3-dispatch": "1", - "d3-selection": "1" - } - }, - "d3-dsv": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.2.0.tgz", - "integrity": "sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==", - "requires": { - "commander": "2", - "iconv-lite": "0.4", - "rw": "1" - } - }, - "d3-ease": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.7.tgz", - "integrity": "sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==" - }, - "d3-fetch": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.2.0.tgz", - "integrity": "sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==", - "requires": { - "d3-dsv": "1" - } - }, - "d3-force": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", - "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", - "requires": { - "d3-collection": "1", - "d3-dispatch": "1", - "d3-quadtree": "1", - "d3-timer": "1" - } - }, - "d3-format": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", - "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==" - }, - "d3-geo": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz", - "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", - "requires": { - "d3-array": "1" - } - }, - "d3-hierarchy": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", - "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==" - }, - "d3-interpolate": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", - "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", - "requires": { - "d3-color": "1" - } - }, - "d3-path": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", - "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" - }, - "d3-polygon": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.6.tgz", - "integrity": "sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==" - }, - "d3-quadtree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz", - "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==" - }, - "d3-random": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz", - "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==" - }, - "d3-scale": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", - "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", - "requires": { - "d3-array": "^1.2.0", - "d3-collection": "1", - "d3-format": "1", - "d3-interpolate": "1", - "d3-time": "1", - "d3-time-format": "2" - } - }, - "d3-scale-chromatic": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", - "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", - "requires": { - "d3-color": "1", - "d3-interpolate": "1" - } - }, - "d3-selection": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz", - "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==" - }, - "d3-shape": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", - "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", - "requires": { - "d3-path": "1" - } - }, - "d3-time": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", - "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" - }, - "d3-time-format": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", - "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", - "requires": { - "d3-time": "1" - } - }, - "d3-timer": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", - "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==" - }, - "d3-transition": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.3.2.tgz", - "integrity": "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==", - "requires": { - "d3-color": "1", - "d3-dispatch": "1", - "d3-ease": "1", - "d3-interpolate": "1", - "d3-selection": "^1.1.0", - "d3-timer": "1" - } - }, - "d3-voronoi": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", - "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" - }, - "d3-zoom": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz", - "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==", - "requires": { - "d3-dispatch": "1", - "d3-drag": "1", - "d3-interpolate": "1", - "d3-selection": "1", - "d3-transition": "1" - } - }, - "dagre": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", - "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", - "requires": { - "graphlib": "^2.1.8", - "lodash": "^4.17.15" - } - }, - "dagre-d3": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/dagre-d3/-/dagre-d3-0.6.4.tgz", - "integrity": "sha512-e/6jXeCP7/ptlAM48clmX4xTZc5Ek6T6kagS7Oz2HrYSdqcLZFLqpAfh7ldbZRFfxCZVyh61NEPR08UQRVxJzQ==", - "requires": { - "d3": "^5.14", - "dagre": "^0.8.5", - "graphlib": "^2.1.8", - "lodash": "^4.17.15" - } - }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, "requires": { "assert-plus": "^1.0.0" } @@ -2418,6 +1359,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, "requires": { "ms": "2.1.2" } @@ -2437,25 +1379,14 @@ "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" - }, - "decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", - "requires": { - "mimic-response": "^2.0.0" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true }, "deepmerge": { "version": "4.2.2", @@ -2507,22 +1438,8 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true }, "detect-newline": { "version": "3.1.0", @@ -2530,42 +1447,12 @@ "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true }, - "diff-match-patch": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", - "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" - }, "diff-sequences": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", "dev": true }, - "dom-serializer": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.2.0.tgz", - "integrity": "sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA==", - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "entities": "^2.0.0" - }, - "dependencies": { - "domhandler": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.0.0.tgz", - "integrity": "sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA==", - "requires": { - "domelementtype": "^2.1.0" - } - } - } - }, - "domelementtype": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz", - "integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==" - }, "domexception": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", @@ -2583,38 +1470,11 @@ } } }, - "domhandler": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", - "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", - "requires": { - "domelementtype": "^2.0.1" - } - }, - "domutils": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.4.4.tgz", - "integrity": "sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA==", - "requires": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0" - }, - "dependencies": { - "domhandler": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.0.0.tgz", - "integrity": "sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA==", - "requires": { - "domelementtype": "^2.1.0" - } - } - } - }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -2631,45 +1491,15 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, - "encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "requires": { - "iconv-lite": "^0.6.2" - }, - "dependencies": { - "iconv-lite": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", - "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - } - } - }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, "requires": { "once": "^1.4.0" } }, - "entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" - }, - "entity-decode": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/entity-decode/-/entity-decode-2.0.2.tgz", - "integrity": "sha512-5CCY/3ci4MC1m2jlumNjWd7VBFt4VfFnmSqSNmVcXq4gxM3Vmarxtt+SvmBnzwLS669MWdVuXboNVj1qN2esVg==", - "requires": { - "he": "^1.1.1" - } - }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -2679,11 +1509,6 @@ "is-arrayish": "^0.2.1" } }, - "es6-promise-pool": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/es6-promise-pool/-/es6-promise-pool-2.5.0.tgz", - "integrity": "sha1-FHxhKza0fxBQJ/nSv1SlmKmdnMs=" - }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -2699,6 +1524,7 @@ "version": "1.14.3", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dev": true, "requires": { "esprima": "^4.0.1", "estraverse": "^4.2.0", @@ -2710,22 +1536,20 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true }, "estraverse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, - "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true }, "exec-sh": { "version": "0.3.4", @@ -2804,11 +1628,6 @@ } } }, - "expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" - }, "expect": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz", @@ -2826,7 +1645,8 @@ "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true }, "extend-shallow": { "version": "3.0.2", @@ -2917,22 +1737,26 @@ "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true }, "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true }, "fb-watchman": { "version": "2.0.1", @@ -2943,20 +1767,11 @@ "bser": "2.1.1" } }, - "file-type": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-10.11.0.tgz", - "integrity": "sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw==" - }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" - }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, "requires": { "to-regex-range": "^5.0.1" } @@ -2971,32 +1786,6 @@ "path-exists": "^4.0.0" } }, - "follow-redirects": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz", - "integrity": "sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA==" - }, - "font-awesome-filetypes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/font-awesome-filetypes/-/font-awesome-filetypes-2.1.0.tgz", - "integrity": "sha512-U6hi14GRjfZFIWsTNyVmCBuHyPhiizWEKVbaQqHipKQv3rA1l1PNvmKulzpqxonFnQMToty5ZhfWbc/0IjLDGA==" - }, - "for-each-property": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/for-each-property/-/for-each-property-0.0.4.tgz", - "integrity": "sha1-z6hXrsFCLh0Sb/CHhPz2Jim8g/Y=", - "requires": { - "get-prototype-chain": "^1.0.1" - } - }, - "for-each-property-deep": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/for-each-property-deep/-/for-each-property-deep-0.0.3.tgz", - "integrity": "sha1-MTCaSvw4qcygbxsiP1PWSm0IP60=", - "requires": { - "for-each-property": "0.0.4" - } - }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -3006,12 +1795,14 @@ "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true }, "form-data": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", @@ -3027,11 +1818,6 @@ "map-cache": "^0.2.2" } }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, "fs-extra": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", @@ -3043,102 +1829,25 @@ "universalify": "^1.0.0" } }, - "fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "requires": { - "minipass": "^2.6.0" - } - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true }, "fsevents": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.1.tgz", "integrity": "sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw==", + "dev": true, "optional": true }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "optional": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "dependencies": { - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "optional": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3156,16 +1865,6 @@ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true }, - "get-prototype-chain": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-prototype-chain/-/get-prototype-chain-1.0.1.tgz", - "integrity": "sha1-oXGhFeoeSQbG7ThDofABwYUQQW8=" - }, - "get-stdin": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz", - "integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=" - }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -3185,28 +1884,16 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, "requires": { "assert-plus": "^1.0.0" } }, - "gettext-parser": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-1.4.0.tgz", - "integrity": "sha512-sedZYLHlHeBop/gZ1jdg59hlUEcpcZJofLq2JFwJT1zTqAU3l2wFv6IsuwFHGqbiT9DWzMUW4/em2+hspnmMMA==", - "requires": { - "encoding": "^0.1.12", - "safe-buffer": "^5.1.1" - } - }, - "github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" - }, "glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -3216,14 +1903,6 @@ "path-is-absolute": "^1.0.0" } }, - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "requires": { - "is-glob": "^4.0.1" - } - }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -3235,28 +1914,24 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" }, - "graphlib": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", - "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", - "requires": { - "lodash": "^4.17.15" - } - }, "growly": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=" + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", + "dev": true, + "optional": true }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true }, "har-validator": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "dev": true, "requires": { "ajv": "^6.12.3", "har-schema": "^2.0.0" @@ -3277,11 +1952,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -3334,16 +2004,6 @@ } } }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" - }, - "highlight.js": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.5.0.tgz", - "integrity": "sha512-xTmvd9HiIHR6L53TMC7TKolEj65zG1XU+Onr8oi86mYa+nLcIbxTTWkpW7CsEwv/vK7u1zb8alZIMLDqqN6KTw==" - }, "hosted-git-info": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", @@ -3359,70 +2019,17 @@ "whatwg-encoding": "^1.0.5" } }, - "html-entities": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz", - "integrity": "sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==" - }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "html-minifier": { - "version": "3.5.21", - "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz", - "integrity": "sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==", - "requires": { - "camel-case": "3.0.x", - "clean-css": "4.2.x", - "commander": "2.17.x", - "he": "1.2.x", - "param-case": "2.1.x", - "relateurl": "0.2.x", - "uglify-js": "3.4.x" - }, - "dependencies": { - "commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==" - }, - "uglify-js": { - "version": "3.4.10", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz", - "integrity": "sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==", - "requires": { - "commander": "~2.19.0", - "source-map": "~0.6.1" - }, - "dependencies": { - "commander": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", - "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==" - } - } - } - } - }, - "http-errors": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz", - "integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - } - }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, "requires": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -3432,83 +2039,18 @@ "human-signals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==" + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } }, - "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" - }, - "ignore-walk": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", - "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", - "requires": { - "minimatch": "^3.0.4" - } - }, - "image-data-uri": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/image-data-uri/-/image-data-uri-2.0.1.tgz", - "integrity": "sha512-BZh721F2Q5TwBdwpiqrBrHEdj8daj8KuMZK/DOCyqQlz1CqFhhuZWbK5ZCUnAvFJr8LaKHTaWl9ja3/a3DC2Ew==", - "requires": { - "fs-extra": "^0.26.7", - "magicli": "0.0.8", - "mime-types": "^2.1.18", - "request": "^2.88.0" - }, - "dependencies": { - "fs-extra": { - "version": "0.26.7", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.26.7.tgz", - "integrity": "sha1-muH92UiXeY7at20JGM9C0MMYT6k=", - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^2.1.0", - "klaw": "^1.0.0", - "path-is-absolute": "^1.0.0", - "rimraf": "^2.2.8" - } - }, - "jsonfile": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "image-type": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/image-type/-/image-type-3.1.0.tgz", - "integrity": "sha512-edYRXKQ3WD2yHXFGUbwoJVn5v7j1A6Z505uZUYIfzCwOOhPGLYSc3VOucF9fqbsaUbgb37DdjOU+WV4uo7ZooQ==", - "requires": { - "file-type": "^10.9.0" - } - }, - "immer": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/immer/-/immer-7.0.15.tgz", - "integrity": "sha512-yM7jo9+hvYgvdCQdqvhCNRRio0SCXc8xDPzA25SvKWa7b1WVPjLwQs1VYU5JPXjcJPTqAa5NP5dqpORGYBQ2AA==" - }, "import-local": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", @@ -3529,6 +2071,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -3537,147 +2080,14 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "inspect-function": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/inspect-function/-/inspect-function-0.2.2.tgz", - "integrity": "sha1-hdoMUli8TDMK4yg7Z0fgdZ2QpjU=", - "requires": { - "split-skip": "0.0.1", - "unpack-string": "0.0.2" - }, - "dependencies": { - "split-skip": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/split-skip/-/split-skip-0.0.1.tgz", - "integrity": "sha1-gK2ONumOV2RUzDtmfB3SXYZejwA=" - } - } - }, - "inspect-parameters-declaration": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/inspect-parameters-declaration/-/inspect-parameters-declaration-0.0.9.tgz", - "integrity": "sha512-c3jrKKA1rwwrsjdGMAo2hFWV0vNe3/RKHxpE/OBt41LP3ynOVI1qmgxpZYK5SQu3jtWCyaho8L7AZzCjJ4mEUw==", - "requires": { - "magicli": "0.0.5", - "split-skip": "0.0.2", - "stringify-parameters": "0.0.4", - "unpack-string": "0.0.2" - }, - "dependencies": { - "magicli": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/magicli/-/magicli-0.0.5.tgz", - "integrity": "sha1-zufQ+7THBRiqyxHsPrfiX/SaSSE=", - "requires": { - "commander": "^2.9.0", - "get-stdin": "^5.0.1", - "inspect-function": "^0.2.1", - "pipe-functions": "^1.2.0" - } - } - } - }, - "inspect-property": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/inspect-property/-/inspect-property-0.0.6.tgz", - "integrity": "sha512-LgjHkRl9W6bj2n+kWrAOgvCYPTYt+LanE4rtd/vKNq6yEb+SvVV7UTLzoSPpDX6/U1cAz7VfqPr+lPAIz7wHaQ==", - "requires": { - "for-each-property": "0.0.4", - "for-each-property-deep": "0.0.3", - "inspect-function": "^0.3.1" - }, - "dependencies": { - "inspect-function": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/inspect-function/-/inspect-function-0.3.4.tgz", - "integrity": "sha512-s0RsbJqK/sNZ+U1mykGoTickog3ea1A9Qk4mXniogOBu4PgkkZ56elScO7QC/r8D94lhGmJ2NyDI1ipOA/uq/g==", - "requires": { - "inspect-parameters-declaration": "0.0.8", - "magicli": "0.0.8", - "split-skip": "0.0.1", - "stringify-parameters": "0.0.4", - "unpack-string": "0.0.2" - } - }, - "inspect-parameters-declaration": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/inspect-parameters-declaration/-/inspect-parameters-declaration-0.0.8.tgz", - "integrity": "sha512-W4QzN1LgFmasKOM+NoLlDd2OAZM3enNZlVUOXoGQKmYBDFgxoPDOyebF55ALaf8avyM9TavNwibXxg347RrzCg==", - "requires": { - "magicli": "0.0.5", - "split-skip": "0.0.2", - "stringify-parameters": "0.0.4", - "unpack-string": "0.0.2" - }, - "dependencies": { - "inspect-function": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/inspect-function/-/inspect-function-0.2.2.tgz", - "integrity": "sha1-hdoMUli8TDMK4yg7Z0fgdZ2QpjU=", - "requires": { - "split-skip": "0.0.1", - "unpack-string": "0.0.2" - }, - "dependencies": { - "split-skip": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/split-skip/-/split-skip-0.0.1.tgz", - "integrity": "sha1-gK2ONumOV2RUzDtmfB3SXYZejwA=" - } - } - }, - "magicli": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/magicli/-/magicli-0.0.5.tgz", - "integrity": "sha1-zufQ+7THBRiqyxHsPrfiX/SaSSE=", - "requires": { - "commander": "^2.9.0", - "get-stdin": "^5.0.1", - "inspect-function": "^0.2.1", - "pipe-functions": "^1.2.0" - } - }, - "split-skip": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/split-skip/-/split-skip-0.0.2.tgz", - "integrity": "sha1-2J2Iu9L3Pka1FYqjcKVhIk6A1GE=" - } - } - }, - "split-skip": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/split-skip/-/split-skip-0.0.1.tgz", - "integrity": "sha1-gK2ONumOV2RUzDtmfB3SXYZejwA=" - } - } + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "ip-regex": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=" - }, - "is-absolute": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.2.6.tgz", - "integrity": "sha1-IN5p89uULvLYe5wto28XIjWxtes=", - "requires": { - "is-relative": "^0.2.1", - "is-windows": "^0.2.0" - }, - "dependencies": { - "is-windows": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", - "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=" - } - } + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true }, "is-accessor-descriptor": { "version": "0.1.6", @@ -3705,18 +2115,11 @@ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", "dev": true }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "requires": { - "binary-extensions": "^2.0.0" - } - }, "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true }, "is-ci": { "version": "2.0.0", @@ -3778,7 +2181,9 @@ "is-docker": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", - "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==" + "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==", + "dev": true, + "optional": true }, "is-extendable": { "version": "0.1.1", @@ -3786,11 +2191,6 @@ "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", "dev": true }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -3802,18 +2202,11 @@ "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "dev": true }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "requires": { - "is-extglob": "^2.1.1" - } - }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true }, "is-plain-object": { "version": "2.0.4", @@ -3830,31 +2223,17 @@ "integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=", "dev": true }, - "is-relative": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-0.2.1.tgz", - "integrity": "sha1-0n9MfVFtF1+2ENuEu+7yPDvJeqU=", - "requires": { - "is-unc-path": "^0.1.1" - } - }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "is-unc-path": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-0.1.2.tgz", - "integrity": "sha1-arBTpyVzwQJQ/0FqOBTDUXivObk=", - "requires": { - "unc-path-regex": "^0.1.0" - } + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true }, "is-windows": { "version": "1.0.2", @@ -3866,34 +2245,23 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "optional": true, "requires": { "is-docker": "^2.0.0" } }, - "is2": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/is2/-/is2-0.0.9.tgz", - "integrity": "sha1-EZVW0dFlGkG6EFr4AyZ8gLKZ9ik=", - "requires": { - "deep-is": "0.1.2" - }, - "dependencies": { - "deep-is": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.2.tgz", - "integrity": "sha1-nO1l6gvAsJ9CptecGxkD+dkTzBg=" - } - } - }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true }, "isobject": { "version": "3.0.1", @@ -3904,7 +2272,8 @@ "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true }, "istanbul-lib-coverage": { "version": "3.0.0", @@ -4612,15 +2981,11 @@ "supports-color": "^7.0.0" } }, - "jmespath": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", - "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true }, "js-yaml": { "version": "3.14.1", @@ -4635,7 +3000,8 @@ "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true }, "jsdom": { "version": "16.4.0", @@ -4686,17 +3052,20 @@ "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true }, "json5": { "version": "2.1.3", @@ -4727,6 +3096,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -4734,33 +3104,12 @@ "verror": "1.10.0" } }, - "katex": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.12.0.tgz", - "integrity": "sha512-y+8btoc/CK70XqcHqjxiGWBOeIL8upbS0peTPXTvgrh21n1RiWWcIpSWM+4uXq+IAgNh9YYQWdc7LVDPDAEEAg==", - "requires": { - "commander": "^2.19.0" - } - }, - "khroma": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/khroma/-/khroma-1.2.0.tgz", - "integrity": "sha512-DlKk5y243dujy8fOH02aRnnewLfiHJV0s8aXaVrCohgBf3s7fEAn6gc6LLQ21agODlFZS8ufrn+juu70uCA9Tw==" - }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, - "klaw": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", - "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", - "requires": { - "graceful-fs": "^4.1.9" - } - }, "kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -4773,15 +3122,11 @@ "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true }, - "levenshtein": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/levenshtein/-/levenshtein-1.0.5.tgz", - "integrity": "sha1-ORFzepy1baNF0Aj1V4LG8TiXm6M=" - }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, "requires": { "prelude-ls": "~1.1.2", "type-check": "~0.3.2" @@ -4793,14 +3138,6 @@ "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", "dev": true }, - "linkify-it": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", - "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", - "requires": { - "uc.micro": "^1.0.1" - } - }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -4813,110 +3150,24 @@ "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" - }, - "lodash-es": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.20.tgz", - "integrity": "sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA==" - }, - "lodash.padend": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz", - "integrity": "sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4=" - }, - "lodash.repeat": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/lodash.repeat/-/lodash.repeat-4.1.0.tgz", - "integrity": "sha1-/H3oEx2MisB+S0n3T/6CnR8r7EQ=" + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" - }, - "lodash.toarray": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", - "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=" - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "lower-case": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", - "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=" + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "requires": { "yallist": "^4.0.0" } }, - "magicli": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/magicli/-/magicli-0.0.8.tgz", - "integrity": "sha512-x/eBenweAHF+DsYy172sK4doRxZl0yrJnfxhLJiN7H6hPM3Ya0PfI6uBZshZ3ScFFSQD7HXgBqMdbnXKEZsO1g==", - "requires": { - "cliss": "0.0.2", - "find-up": "^2.1.0", - "for-each-property": "0.0.4", - "inspect-property": "0.0.6" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - } - } - }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -4958,160 +3209,11 @@ "object-visit": "^1.0.0" } }, - "markdown-it": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz", - "integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==", - "requires": { - "argparse": "^1.0.7", - "entities": "~2.0.0", - "linkify-it": "^2.0.0", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - }, - "dependencies": { - "entities": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", - "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==" - } - } - }, - "markdown-it-abbr": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/markdown-it-abbr/-/markdown-it-abbr-1.0.4.tgz", - "integrity": "sha1-1mtTZFIcuz3Yqlna37ovtoZcj9g=" - }, - "markdown-it-anchor": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.3.0.tgz", - "integrity": "sha512-/V1MnLL/rgJ3jkMWo84UR+K+jF1cxNG1a+KwqeXqTIJ+jtA8aWSHuigx8lTzauiIjBDbwF3NcWQMotd0Dm39jA==" - }, - "markdown-it-deflist": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/markdown-it-deflist/-/markdown-it-deflist-2.1.0.tgz", - "integrity": "sha512-3OuqoRUlSxJiuQYu0cWTLHNhhq2xtoSFqsZK8plANg91+RJQU1ziQ6lA2LzmFAEes18uPBsHZpcX6We5l76Nzg==" - }, - "markdown-it-emoji": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz", - "integrity": "sha1-m+4OmpkKljupbfaYDE/dsF37Tcw=" - }, - "markdown-it-expand-tabs": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/markdown-it-expand-tabs/-/markdown-it-expand-tabs-1.0.13.tgz", - "integrity": "sha512-ODpk98FWkGIq2vkwm2NOLt4G6TRgy3M9eTa9SFm06pUyOd0zjjYAwkhsjiCDU42pzKuz0ChiwBO0utuOj3LNOA==", - "requires": { - "lodash.repeat": "^4.0.0" - } - }, - "markdown-it-footnote": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/markdown-it-footnote/-/markdown-it-footnote-3.0.2.tgz", - "integrity": "sha512-JVW6fCmZWjvMdDQSbOT3nnOQtd9iAXmw7hTSh26+v42BnvXeVyGMDBm5b/EZocMed2MbCAHiTX632vY0FyGB8A==" - }, - "markdown-it-ins": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/markdown-it-ins/-/markdown-it-ins-3.0.1.tgz", - "integrity": "sha512-32SSfZqSzqyAmmQ4SHvhxbFqSzPDqsZgMHDwxqPzp+v+t8RsmqsBZRG+RfRQskJko9PfKC2/oxyOs4Yg/CfiRw==" - }, - "markdown-it-mark": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/markdown-it-mark/-/markdown-it-mark-3.0.1.tgz", - "integrity": "sha512-HyxjAu6BRsdt6Xcv6TKVQnkz/E70TdGXEFHRYBGLncRE9lBFwDNLVtFojKxjJWgJ+5XxUwLaHXy+2sGBbDn+4A==" - }, - "markdown-it-multimd-table": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/markdown-it-multimd-table/-/markdown-it-multimd-table-4.0.3.tgz", - "integrity": "sha512-uVLriNj6doq1dGyJppQdwbaGcK6uSzbrk7osxRHjOmZBeShgMtPS6/d+pnIKkohOjaRyP9e5kwTAlAIe/lEaIQ==", - "requires": { - "markdown-it": "^11.0.0" - }, - "dependencies": { - "entities": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", - "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==" - }, - "linkify-it": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.2.tgz", - "integrity": "sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ==", - "requires": { - "uc.micro": "^1.0.1" - } - }, - "markdown-it": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-11.0.1.tgz", - "integrity": "sha512-aU1TzmBKcWNNYvH9pjq6u92BML+Hz3h5S/QpfTFwiQF852pLT+9qHsrhM9JYipkOXZxGn+sGH8oyJE9FD9WezQ==", - "requires": { - "argparse": "^1.0.7", - "entities": "~2.0.0", - "linkify-it": "^3.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - } - } - } - }, - "markdown-it-sub": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/markdown-it-sub/-/markdown-it-sub-1.0.0.tgz", - "integrity": "sha1-N1/WAm6ufdywEkl/ZBEZXqHjr+g=" - }, - "markdown-it-sup": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/markdown-it-sup/-/markdown-it-sup-1.0.0.tgz", - "integrity": "sha1-y5yf+RpSVawI8/09YyhuFd8KH8M=" - }, - "markdown-it-toc-done-right": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/markdown-it-toc-done-right/-/markdown-it-toc-done-right-4.2.0.tgz", - "integrity": "sha512-UB/IbzjWazwTlNAX0pvWNlJS8NKsOQ4syrXZQ/C72j+jirrsjVRT627lCaylrKJFBQWfRsPmIVQie8x38DEhAQ==" - }, - "md5": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "requires": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } - }, - "md5-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-4.0.0.tgz", - "integrity": "sha512-UC0qFwyAjn4YdPpKaDNw6gNxRf7Mcx7jC1UGCY4boCzgvU2Aoc1mOGzTtrjjLKhM5ivsnhoKpQVxKPp+1j1qwg==" - }, - "mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" - }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" - }, - "mermaid": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.9.0.tgz", - "integrity": "sha512-J582tyE1vkdNu4BGgfwXnFo4Mu6jpuc4uK96mIenavaak9kr4T5gaMmYCo/7edwq/vTBkx/soZ5LcJo5WXZ1BQ==", - "requires": { - "@braintree/sanitize-url": "^3.1.0", - "d3": "^5.7.0", - "dagre": "^0.8.4", - "dagre-d3": "^0.6.4", - "entity-decode": "^2.0.2", - "graphlib": "^2.1.7", - "he": "^1.2.0", - "khroma": "^1.1.0", - "minify": "^4.1.1", - "moment-mini": "^2.22.1", - "stylis": "^3.5.2" - } + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true }, "micromatch": { "version": "4.0.2", @@ -5126,12 +3228,14 @@ "mime-db": { "version": "1.45.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", - "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==" + "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==", + "dev": true }, "mime-types": { "version": "2.1.28", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz", "integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==", + "dev": true, "requires": { "mime-db": "1.45.0" } @@ -5139,47 +3243,14 @@ "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" - }, - "mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" - }, - "minify": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/minify/-/minify-4.1.3.tgz", - "integrity": "sha512-ykuscavxivSmVpcCzsXmsVTukWYLUUtPhHj0w2ILvHDGqC+hsuTCihBn9+PJBd58JNvWTNg9132J9nrrI2anzA==", - "requires": { - "clean-css": "^4.1.6", - "css-b64-images": "~0.2.5", - "debug": "^4.1.0", - "html-minifier": "^4.0.0", - "terser": "^4.0.0", - "try-catch": "^2.0.0", - "try-to-catch": "^1.0.2" - }, - "dependencies": { - "html-minifier": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz", - "integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==", - "requires": { - "camel-case": "^3.0.0", - "clean-css": "^4.2.1", - "commander": "^2.19.0", - "he": "^1.2.0", - "param-case": "^2.1.1", - "relateurl": "^0.2.7", - "uglify-js": "^3.5.1" - } - } - } + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5187,31 +3258,8 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - }, - "dependencies": { - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - } - } - }, - "minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "requires": { - "minipass": "^2.9.0" - } + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true }, "mixin-deep": { "version": "1.3.2", @@ -5234,60 +3282,11 @@ } } }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "requires": { - "minimist": "^1.2.5" - } - }, - "mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" - }, - "moment": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" - }, - "moment-mini": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/moment-mini/-/moment-mini-2.24.0.tgz", - "integrity": "sha512-9ARkWHBs+6YJIvrIp0Ik5tyTTtP9PoV0Ssu2Ocq5y9v8+NOOpWiRshAp8c4rZVWTOe+157on/5G+zj5pwIQFEQ==" - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "multiparty": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.2.tgz", - "integrity": "sha512-NtZLjlvsjcoGrzojtwQwn/Tm90aWJ6XXtPppYF4WmOk/6ncdwMMKggFY2NlRRN9yiCEIVxpOfPWahVEG2HAG8Q==", - "requires": { - "http-errors": "~1.8.0", - "safe-buffer": "5.2.1", - "uid-safe": "2.1.5" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - } - } - }, - "mustache": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.1.0.tgz", - "integrity": "sha512-0FsgP/WVq4mKyjolIyX+Z9Bd+3WS8GOwoUTyKXT5cTYMGeauNTi2HPCwERqseC1IHAy0Z7MDZnJBfjabd4O8GQ==" - }, - "nanoid": { - "version": "3.1.20", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", - "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "nanomatch": { "version": "1.2.13", @@ -5308,134 +3307,18 @@ "to-regex": "^3.0.1" } }, - "napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" - }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "needle": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.6.0.tgz", - "integrity": "sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg==", - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - } - } - }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, - "no-case": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", - "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", - "requires": { - "lower-case": "^1.1.1" - } - }, - "node-abi": { - "version": "2.19.3", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.19.3.tgz", - "integrity": "sha512-9xZrlyfvKhWme2EXFKQhZRp1yNWT/uI1luYPr3sFl+H4keYY4xR+1jO7mvTTijIsHf1M+QDe9uWuKeEpLInIlg==", - "requires": { - "semver": "^5.4.1" - } - }, - "node-addon-api": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz", - "integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==" - }, - "node-emoji": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz", - "integrity": "sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==", - "requires": { - "lodash.toarray": "^4.4.0" - } - }, - "node-fetch": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", - "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", - "requires": { - "encoding": "^0.1.11", - "is-stream": "^1.0.1" - } - }, - "node-gyp": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", - "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", - "optional": true, - "requires": { - "fstream": "^1.0.0", - "glob": "^7.0.3", - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "nopt": "2 || 3", - "npmlog": "0 || 1 || 2 || 3 || 4", - "osenv": "0", - "request": "^2.87.0", - "rimraf": "2", - "semver": "~5.3.0", - "tar": "^2.0.0", - "which": "1" - }, - "dependencies": { - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", - "optional": true - }, - "tar": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", - "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", - "optional": true, - "requires": { - "block-stream": "*", - "fstream": "^1.0.12", - "inherits": "2" - } - } - } - }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -5452,6 +3335,8 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz", "integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==", + "dev": true, + "optional": true, "requires": { "growly": "^1.3.0", "is-wsl": "^2.2.0", @@ -5465,6 +3350,8 @@ "version": "7.3.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "optional": true, "requires": { "lru-cache": "^6.0.0" } @@ -5473,72 +3360,14 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "optional": true, "requires": { "isexe": "^2.0.0" } } } }, - "node-persist": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/node-persist/-/node-persist-2.1.0.tgz", - "integrity": "sha1-5lK784haBNrWo1PXQXYXfIORRwc=", - "requires": { - "is-absolute": "^0.2.6", - "mkdirp": "~0.5.1", - "q": "~1.1.1" - } - }, - "node-pre-gyp": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", - "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - }, - "dependencies": { - "nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "noop-logger": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", - "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=" - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "optional": true, - "requires": { - "abbrev": "1" - } - }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -5554,30 +3383,8 @@ "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, - "npm-bundled": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", - "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" - }, - "npm-packlist": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", - "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true }, "npm-run-path": { "version": "2.0.2", @@ -5588,36 +3395,17 @@ "path-key": "^2.0.0" } }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, "nwsapi": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", - "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==" + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", + "dev": true }, "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true }, "object-copy": { "version": "0.1.0", @@ -5650,42 +3438,6 @@ } } }, - "object-to-arguments": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/object-to-arguments/-/object-to-arguments-0.0.8.tgz", - "integrity": "sha512-BfWfuAwuhdH1bhMG5EG90WE/eckkBhBvnke8eSEkCDXoLE9Jk5JwYGTbCx1ehGwV48HvBkn62VukPBdlMUOY9w==", - "requires": { - "inspect-parameters-declaration": "0.0.10", - "magicli": "0.0.5", - "split-skip": "0.0.2", - "stringify-parameters": "0.0.4", - "unpack-string": "0.0.2" - }, - "dependencies": { - "inspect-parameters-declaration": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/inspect-parameters-declaration/-/inspect-parameters-declaration-0.0.10.tgz", - "integrity": "sha512-L8/Bvt9iDXQTZ63xY5/MAyvzz+FagR/qGh1kIXvUpsno3AAE0Z95d6QO51zrcMGaEGpwh/57idfMxTxbvRmytg==", - "requires": { - "magicli": "0.0.5", - "split-skip": "0.0.2", - "stringify-parameters": "0.0.4", - "unpack-string": "0.0.2" - } - }, - "magicli": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/magicli/-/magicli-0.0.5.tgz", - "integrity": "sha1-zufQ+7THBRiqyxHsPrfiX/SaSSE=", - "requires": { - "commander": "^2.9.0", - "get-stdin": "^5.0.1", - "inspect-function": "^0.2.1", - "pipe-functions": "^1.2.0" - } - } - } - }, "object-visit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", @@ -5708,6 +3460,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, "requires": { "wrappy": "1" } @@ -5716,6 +3469,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, "requires": { "mimic-fn": "^2.1.0" } @@ -5724,6 +3478,7 @@ "version": "0.8.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, "requires": { "deep-is": "~0.1.3", "fast-levenshtein": "~2.0.6", @@ -5733,25 +3488,6 @@ "word-wrap": "~1.2.3" } }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, "p-each-series": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", @@ -5788,14 +3524,6 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, - "param-case": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", - "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", - "requires": { - "no-case": "^2.2.0" - } - }, "parse-json": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", @@ -5829,7 +3557,8 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true }, "path-key": { "version": "2.0.1", @@ -5843,30 +3572,17 @@ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, - "pct-encode": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pct-encode/-/pct-encode-1.0.2.tgz", - "integrity": "sha1-uZt7BE1r18OeSDmnqAEirXUVyqU=" - }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true }, "picomatch": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - }, - "pipe-functions": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/pipe-functions/-/pipe-functions-1.3.0.tgz", - "integrity": "sha512-6Rtbp7criZRwedlvWbUYxqlqJoAlMvYHo2UcRWq79xZ54vZcaNHpVBOcWkX3ErT2aUA69tv+uiv4zKJbhD/Wgg==" + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true }, "pirates": { "version": "4.0.1", @@ -5886,55 +3602,17 @@ "find-up": "^4.0.0" } }, - "pn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", - "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==" - }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true }, - "prebuild-install": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.6.tgz", - "integrity": "sha512-s8Aai8++QQGi4sSbs/M1Qku62PFK49Jm1CbgXklGz4nmHveDq0wzJkg7Na5QbnO1uNH8K7iqx2EQ/mV0MZEmOg==", - "requires": { - "detect-libc": "^1.0.3", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", - "node-abi": "^2.7.0", - "noop-logger": "^0.1.1", - "npmlog": "^4.0.1", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^3.0.3", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0", - "which-pm-runs": "^1.0.0" - }, - "dependencies": { - "simple-get": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", - "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", - "requires": { - "decompress-response": "^4.2.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - } - } - }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true }, "pretty-format": { "version": "26.6.2", @@ -5948,19 +3626,6 @@ "react-is": "^17.0.1" } }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "requires": { - "asap": "~2.0.3" - } - }, "prompts": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz", @@ -5974,12 +3639,14 @@ "psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "dev": true }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -5988,57 +3655,14 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "q": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/q/-/q-1.1.2.tgz", - "integrity": "sha1-Y1fikSBnAdmfGXq4TlforRlvKok=" + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "query-string": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", - "requires": { - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - } - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" - }, - "querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" - }, - "random-bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", - "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "re-reselect": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/re-reselect/-/re-reselect-4.0.0.tgz", - "integrity": "sha512-wuygyq8TXUlSdVXv2kigXxQNOgdb9m7LbIjwfTNGSpaY1riLd5e+VeQjlQMyUtrk0oiyhi1AqIVynworl3qxHA==" + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true }, "react-is": { "version": "17.0.1", @@ -6046,15 +3670,6 @@ "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==", "dev": true }, - "read-chunk": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-2.1.0.tgz", - "integrity": "sha1-agTAkoAF7Z1C4aasVgDhnLx/9lU=", - "requires": { - "pify": "^3.0.0", - "safe-buffer": "^5.1.1" - } - }, "read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -6086,44 +3701,6 @@ "type-fest": "^0.8.1" } }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "readdirp": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", - "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", - "requires": { - "picomatch": "^2.2.1" - } - }, - "reduce-flatten": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-1.0.1.tgz", - "integrity": "sha1-JYx479FT3fk8tWEjf2EYTzaW4yc=" - }, - "redux": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", - "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==", - "requires": { - "lodash": "^4.2.1", - "lodash-es": "^4.2.1", - "loose-envify": "^1.1.0", - "symbol-observable": "^1.0.3" - } - }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -6134,29 +3711,6 @@ "safe-regex": "^1.1.0" } }, - "relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=" - }, - "relative": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/relative/-/relative-3.0.2.tgz", - "integrity": "sha1-Dc2OxUpdNaPBXhBFA9ZTdbWlNn8=", - "requires": { - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "1.0.0" - } - } - } - }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -6179,6 +3733,7 @@ "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "dev": true, "requires": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -6206,6 +3761,7 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, "requires": { "psl": "^1.1.28", "punycode": "^2.1.1" @@ -6214,7 +3770,8 @@ "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true } } }, @@ -6222,6 +3779,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "dev": true, "requires": { "lodash": "^4.17.19" } @@ -6230,6 +3788,7 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", + "dev": true, "requires": { "request-promise-core": "1.1.4", "stealthy-require": "^1.1.1", @@ -6240,6 +3799,7 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, "requires": { "psl": "^1.1.28", "punycode": "^2.1.1" @@ -6258,16 +3818,6 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" - }, - "reselect": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz", - "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==" - }, "resolve": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", @@ -6296,7 +3846,8 @@ "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true }, "ret": { "version": "0.1.15", @@ -6319,15 +3870,11 @@ "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", "dev": true }, - "rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" - }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true }, "safe-regex": { "version": "1.1.0", @@ -6341,7 +3888,8 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true }, "sane": { "version": "4.1.0", @@ -6484,11 +4032,6 @@ } } }, - "sax": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" - }, "saxes": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", @@ -6501,17 +4044,14 @@ "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - }, - "server-destroy": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", - "integrity": "sha1-8Tv5KOQrnD55OD5hzDmYtdFObN0=" + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true }, "set-value": { "version": "2.0.1", @@ -6536,85 +4076,6 @@ } } }, - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "sharp": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.25.4.tgz", - "integrity": "sha512-umSzJJ1oBwIOfwFFt/fJ7JgCva9FvrEU2cbbm7u/3hSDZhXvkME8WE5qpaJqLIe2Har5msF5UG4CzYlEg5o3BQ==", - "requires": { - "color": "^3.1.2", - "detect-libc": "^1.0.3", - "node-addon-api": "^3.0.0", - "npmlog": "^4.1.2", - "prebuild-install": "^5.3.4", - "semver": "^7.3.2", - "simple-get": "^4.0.0", - "tar": "^6.0.2", - "tunnel-agent": "^0.6.0" - }, - "dependencies": { - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" - }, - "fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", - "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", - "requires": { - "yallist": "^4.0.0" - } - }, - "minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "requires": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" - }, - "semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "tar": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz", - "integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==", - "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - } - } - } - }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -6633,57 +4094,15 @@ "shellwords": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==" + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true, + "optional": true }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" - }, - "simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" - }, - "simple-get": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.0.tgz", - "integrity": "sha512-ZalZGexYr3TA0SwySsr5HlgOOinS4Jsa8YB2GJ6lUNAazyAu4KG/VmzMTwAt2YVXzzVj8QmefmAonZIK2BSGcQ==", - "requires": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - }, - "dependencies": { - "decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "requires": { - "mimic-response": "^3.1.0" - } - }, - "mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" - } - } - }, - "simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", - "requires": { - "is-arrayish": "^0.3.1" - }, - "dependencies": { - "is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - } - } + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true }, "sisteransi": { "version": "1.0.5", @@ -6828,12 +4247,14 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true }, "source-map-resolve": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "dev": true, "requires": { "atob": "^2.1.2", "decode-uri-component": "^0.2.0", @@ -6846,6 +4267,7 @@ "version": "0.5.19", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -6854,7 +4276,8 @@ "source-map-url": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true }, "spdx-correct": { "version": "3.1.1", @@ -6888,11 +4311,6 @@ "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==", "dev": true }, - "split-skip": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/split-skip/-/split-skip-0.0.2.tgz", - "integrity": "sha1-2J2Iu9L3Pka1FYqjcKVhIk6A1GE=" - }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -6905,22 +4323,14 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "sqlite3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.0.1.tgz", - "integrity": "sha512-kh2lTIcYNfmVcvhVJihsYuPj9U0xzBbh6bmqILO2hkryWSC9RRhzYmkIDtJkJ+d8Kg4wZRJ0T1reyHUEspICfg==", - "requires": { - "node-addon-api": "^3.0.0", - "node-gyp": "3.x", - "node-pre-gyp": "^0.11.0" - } + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true }, "sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, "requires": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -6971,20 +4381,11 @@ } } }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" - }, "stealthy-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" - }, - "strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true }, "string-length": { "version": "4.0.1", @@ -6996,20 +4397,6 @@ "strip-ansi": "^6.0.0" } }, - "string-padding": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-padding/-/string-padding-1.0.2.tgz", - "integrity": "sha1-OqrYVbPpc1xeQS3+chmMz5nH9I4=" - }, - "string-to-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string-to-stream/-/string-to-stream-1.1.1.tgz", - "integrity": "sha512-QySF2+3Rwq0SdO3s7BAp4x+c3qsClpPQ6abAmb0DGViiSBAkT5kL6JT2iyzEVP+T1SmzHrQD1TwlP9QAHCc+Sw==", - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.1.0" - } - }, "string-width": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", @@ -7020,36 +4407,6 @@ "strip-ansi": "^6.0.0" } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "stringify-parameters": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/stringify-parameters/-/stringify-parameters-0.0.4.tgz", - "integrity": "sha512-H3L90ERn5UPtkpO8eugnKcLgpIVlvTyUTrcLGm607AV5JDH6z0GymtNLr3gjGlP6I6NB/mxNX9QpY6jEQGLPdQ==", - "requires": { - "magicli": "0.0.5", - "unpack-string": "0.0.2" - }, - "dependencies": { - "magicli": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/magicli/-/magicli-0.0.5.tgz", - "integrity": "sha1-zufQ+7THBRiqyxHsPrfiX/SaSSE=", - "requires": { - "commander": "^2.9.0", - "get-stdin": "^5.0.1", - "inspect-function": "^0.2.1", - "pipe-functions": "^1.2.0" - } - } - } - }, "strip-ansi": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", @@ -7073,17 +4430,8 @@ "strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "stylis": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz", - "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==" + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true }, "supports-color": { "version": "7.2.0", @@ -7104,105 +4452,11 @@ "supports-color": "^7.0.0" } }, - "symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" - }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" - }, - "table-layout": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-0.4.5.tgz", - "integrity": "sha512-zTvf0mcggrGeTe/2jJ6ECkJHAQPIYEwDoqsiqBjI24mvRmQbInK5jq33fyypaCBxX08hMkfmdOqj6haT33EqWw==", - "requires": { - "array-back": "^2.0.0", - "deep-extend": "~0.6.0", - "lodash.padend": "^4.6.1", - "typical": "^2.6.1", - "wordwrapjs": "^3.0.0" - } - }, - "tar": { - "version": "4.4.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", - "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - }, - "dependencies": { - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - } - } - }, - "tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "requires": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "requires": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "tcp-port-used": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-0.1.2.tgz", - "integrity": "sha1-lFDodoyDtBb9TRpqlEnuzL9JbCk=", - "requires": { - "debug": "0.7.4", - "is2": "0.0.9", - "q": "0.9.7" - }, - "dependencies": { - "debug": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz", - "integrity": "sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk=" - }, - "q": { - "version": "0.9.7", - "resolved": "https://registry.npmjs.org/q/-/q-0.9.7.tgz", - "integrity": "sha1-TeLmyzspCIyeTLwDv51C+5bOL3U=" - } - } + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true }, "terminal-link": { "version": "2.1.1", @@ -7214,16 +4468,6 @@ "supports-hyperlinks": "^2.0.0" } }, - "terser": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", - "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", - "requires": { - "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" - } - }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -7289,19 +4533,16 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "requires": { "is-number": "^7.0.0" } }, - "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" - }, "tough-cookie": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", + "dev": true, "requires": { "ip-regex": "^2.1.0", "psl": "^1.1.28", @@ -7317,20 +4558,11 @@ "punycode": "^2.1.1" } }, - "try-catch": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/try-catch/-/try-catch-2.0.1.tgz", - "integrity": "sha512-LsOrmObN/2WdM+y2xG+t16vhYrQsnV8wftXIcIOWZhQcBJvKGYuamJGwnU98A7Jxs2oZNkJztXlphEOoA0DWqg==" - }, - "try-to-catch": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-1.1.1.tgz", - "integrity": "sha512-ikUlS+/BcImLhNYyIgZcEmq4byc31QpC+46/6Jm5ECWkVFhf8SM2Fp/0pMVXPX6vk45SMCwrP4Taxucne8I0VA==" - }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, "requires": { "safe-buffer": "^5.0.1" } @@ -7338,12 +4570,14 @@ "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, "requires": { "prelude-ls": "~1.1.2" } @@ -7375,39 +4609,6 @@ "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==", "dev": true }, - "typical": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", - "integrity": "sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=" - }, - "uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" - }, - "uglify-js": { - "version": "3.12.5", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.12.5.tgz", - "integrity": "sha512-SgpgScL4T7Hj/w/GexjnBHi3Ien9WS1Rpfg5y91WXMj9SY997ZCQU76mH4TpLwwfmMvoOU8wiaRkIf6NaH3mtg==" - }, - "uglifycss": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/uglifycss/-/uglifycss-0.0.29.tgz", - "integrity": "sha512-J2SQ2QLjiknNGbNdScaNZsXgmMGI0kYNrXaDlr4obnPW9ni1jljb1NeEVWAiTgZ8z+EBWP2ozfT9vpy03rjlMQ==" - }, - "uid-safe": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", - "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", - "requires": { - "random-bytes": "~1.0.0" - } - }, - "unc-path-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=" - }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -7425,16 +4626,6 @@ "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==" }, - "unorm": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz", - "integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==" - }, - "unpack-string": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/unpack-string/-/unpack-string-0.0.2.tgz", - "integrity": "sha1-MC7PCCOLATm9Q0pNf9Z83zPKJ10=" - }, "unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", @@ -7475,56 +4666,20 @@ } } }, - "upper-case": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", - "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=" - }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "requires": { "punycode": "^2.1.0" } }, - "uri-template": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uri-template/-/uri-template-1.0.1.tgz", - "integrity": "sha1-FKklo35Nk/diVDKqEWsF5Qyuga0=", - "requires": { - "pct-encode": "~1.0.0" - } - }, "urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" - }, - "url": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", - "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" - } - } - }, - "url-parse": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", - "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", - "requires": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true }, "use": { "version": "3.1.1", @@ -7532,23 +4687,12 @@ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, - "uslug": { - "version": "git+https://github.com/laurent22/uslug.git#ba2834d79beb0435318709958b2f5e817d96674d", - "from": "git+https://github.com/laurent22/uslug.git#emoji-support", - "requires": { - "node-emoji": "^1.10.0", - "unorm": ">= 1.0.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "optional": true }, "v8-to-istanbul": { "version": "7.1.0", @@ -7569,11 +4713,6 @@ } } }, - "valid-url": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", - "integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=" - }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -7588,6 +4727,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, "requires": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -7598,6 +4738,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "dev": true, "requires": { "browser-process-hrtime": "^1.0.0" } @@ -7630,6 +4771,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dev": true, "requires": { "iconv-lite": "0.4.24" } @@ -7637,7 +4779,8 @@ "whatwg-mimetype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true }, "whatwg-url": { "version": "8.4.0", @@ -7654,6 +4797,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, "requires": { "isexe": "^2.0.0" } @@ -7664,61 +4808,11 @@ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "dev": true }, - "which-pm-runs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", - "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=" - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "requires": { - "string-width": "^1.0.2 || 2" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" - }, - "wordwrapjs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-3.0.0.tgz", - "integrity": "sha512-mO8XtqyPvykVCsrwj5MlOVWvSnCdT+C+QVbm6blradR7JExAhbkZ7hZ9A+9NUtwzSqrlUo9a67ws0EiILrvRpw==", - "requires": { - "reduce-flatten": "^1.0.1", - "typical": "^2.6.1" - } + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true }, "wrap-ansi": { "version": "7.0.0", @@ -7733,7 +4827,8 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, "write-file-atomic": { "version": "3.0.3", @@ -7750,38 +4845,20 @@ "ws": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz", - "integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==" + "integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==", + "dev": true }, "xml-name-validator": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" - }, - "xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "dependencies": { - "xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" - } - } - }, - "xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true }, "xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true }, "y18n": { "version": "5.0.5", @@ -7791,7 +4868,8 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "yargs": { "version": "16.2.0", diff --git a/packages/renderer/HtmlToHtml.ts b/packages/renderer/HtmlToHtml.ts index b91cb7d5ce..b93df48731 100644 --- a/packages/renderer/HtmlToHtml.ts +++ b/packages/renderer/HtmlToHtml.ts @@ -1,6 +1,6 @@ import htmlUtils from './htmlUtils'; import linkReplacement from './MdToHtml/linkReplacement'; -import utils from './utils'; +import utils, { ItemIdToUrlHandler } from './utils'; // TODO: fix // import Setting from '@joplin/lib/models/Setting'; @@ -32,6 +32,7 @@ interface RenderOptions { resources: any; postMessageSyntax: string; enableLongPress: boolean; + itemIdToUrl?: ItemIdToUrlHandler; } interface RenderResult { @@ -39,6 +40,13 @@ interface RenderResult { pluginAssets: any[]; } +// https://github.com/es-shims/String.prototype.trimStart/blob/main/implementation.js +function trimStart(s: string): string { + // eslint-disable-next-line no-control-regex + const startWhitespace = /^[\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF]*/; + return s.replace(startWhitespace, ''); +} + export default class HtmlToHtml { private resourceBaseUrl_; @@ -73,7 +81,7 @@ export default class HtmlToHtml { } splitHtml(html: string) { - const trimmedHtml = html.trimStart(); + const trimmedHtml = trimStart(html); if (trimmedHtml.indexOf(''); @@ -109,7 +117,7 @@ export default class HtmlToHtml { html = htmlUtils.processImageTags(html, (data: any) => { if (!data.src) return null; - const r = utils.imageReplacement(this.ResourceModel_, data.src, options.resources, this.resourceBaseUrl_); + const r = utils.imageReplacement(this.ResourceModel_, data.src, options.resources, this.resourceBaseUrl_, options.itemIdToUrl); if (!r) return null; if (typeof r === 'string') { diff --git a/packages/renderer/MarkupToHtml.ts b/packages/renderer/MarkupToHtml.ts index 31990564ae..6d183b5d4e 100644 --- a/packages/renderer/MarkupToHtml.ts +++ b/packages/renderer/MarkupToHtml.ts @@ -8,6 +8,18 @@ export enum MarkupLanguage { Html = 2, } +export interface RenderResultPluginAsset { + name: string; + path: string; + mime: string; +} + +export interface RenderResult { + html: string; + pluginAssets: RenderResultPluginAsset[]; + cssStrings: string[]; +} + export default class MarkupToHtml { static MARKUP_LANGUAGE_MARKDOWN: number = MarkupLanguage.Markdown; @@ -75,7 +87,7 @@ export default class MarkupToHtml { if (r.clearCache) r.clearCache(); } - async render(markupLanguage: MarkupLanguage, markup: string, theme: any, options: any) { + async render(markupLanguage: MarkupLanguage, markup: string, theme: any, options: any): Promise { return this.renderer(markupLanguage).render(markup, theme, options); } diff --git a/packages/renderer/MdToHtml.ts b/packages/renderer/MdToHtml.ts index 4d77dd4a20..ba28c3bafa 100644 --- a/packages/renderer/MdToHtml.ts +++ b/packages/renderer/MdToHtml.ts @@ -3,6 +3,8 @@ import noteStyle from './noteStyle'; import { fileExtension } from './pathUtils'; import setupLinkify from './MdToHtml/setupLinkify'; import validateLinks from './MdToHtml/validateLinks'; +import { ItemIdToUrlHandler } from './utils'; +import { RenderResult, RenderResultPluginAsset } from './MarkupToHtml'; const MarkdownIt = require('markdown-it'); const md5 = require('md5'); @@ -114,18 +116,6 @@ interface PluginContext { currentLinks: Link[]; } -interface RenderResultPluginAsset { - name: string; - path: string; - mime: string; -} - -interface RenderResult { - html: string; - pluginAssets: RenderResultPluginAsset[]; - cssStrings: string[]; -} - export interface RuleOptions { context: PluginContext; theme: any; @@ -157,6 +147,8 @@ export interface RuleOptions { audioPlayerEnabled: boolean; videoPlayerEnabled: boolean; pdfViewerEnabled: boolean; + + itemIdToUrl?: ItemIdToUrlHandler; } export default class MdToHtml { diff --git a/packages/renderer/MdToHtml/linkReplacement.ts b/packages/renderer/MdToHtml/linkReplacement.ts index 81d3c26202..1cacba8ade 100644 --- a/packages/renderer/MdToHtml/linkReplacement.ts +++ b/packages/renderer/MdToHtml/linkReplacement.ts @@ -1,4 +1,4 @@ -import utils from '../utils'; +import utils, { ItemIdToUrlHandler } from '../utils'; const Entities = require('html-entities').AllHtmlEntities; const htmlentities = new Entities().encode; const urlUtils = require('../urlUtils.js'); @@ -12,6 +12,7 @@ export interface Options { plainResourceRendering?: boolean; postMessageSyntax?: string; enableLongPress?: boolean; + itemIdToUrl?: ItemIdToUrlHandler; } export interface LinkReplacementResult { @@ -65,6 +66,8 @@ export default function(href: string, options: Options = null): LinkReplacementR resourceFullPath: null, }; } else { + // If we are rendering a note link, we'll get here too, so in that + // case "resourceId" would actually be the note ID. href = `joplin://${resourceId}`; if (resourceHrefInfo.hash) href += `#${resourceHrefInfo.hash}`; resourceIdAttr = `data-resource-id='${resourceId}'`; @@ -109,7 +112,13 @@ export default function(href: string, options: Options = null): LinkReplacementR if (title) attrHtml.push(`title='${htmlentities(title)}'`); if (mime) attrHtml.push(`type='${htmlentities(mime)}'`); - if (options.plainResourceRendering || options.linkRenderingType === 2) { + let resourceFullPath = resource && options?.ResourceModel?.fullPath ? options.ResourceModel.fullPath(resource) : null; + + if (resourceId && options.itemIdToUrl) { + const url = options.itemIdToUrl(resourceId); + attrHtml.push(`href='${htmlentities(url)}'`); + resourceFullPath = url; + } else if (options.plainResourceRendering || options.linkRenderingType === 2) { icon = ''; attrHtml.push(`href='${htmlentities(href)}'`); } else { @@ -121,6 +130,6 @@ export default function(href: string, options: Options = null): LinkReplacementR html: `${icon}`, resourceReady: true, resource, - resourceFullPath: resource && options?.ResourceModel?.fullPath ? options.ResourceModel.fullPath(resource) : null, + resourceFullPath: resourceFullPath, }; } diff --git a/packages/renderer/MdToHtml/renderMedia.ts b/packages/renderer/MdToHtml/renderMedia.ts index ad8a0f2ef1..ec022fc841 100644 --- a/packages/renderer/MdToHtml/renderMedia.ts +++ b/packages/renderer/MdToHtml/renderMedia.ts @@ -9,12 +9,17 @@ export interface Options { pdfViewerEnabled: boolean; } +function resourceUrl(resourceFullPath: string): string { + if (resourceFullPath.indexOf('http://') === 0 || resourceFullPath.indexOf('https://')) return resourceFullPath; + return `file://${toForwardSlashes(resourceFullPath)}`; +} + export default function(link: Link, options: Options) { const resource = link.resource; if (!link.resourceReady || !resource || !resource.mime) return ''; - const escapedResourcePath = htmlentities(`file://${toForwardSlashes(link.resourceFullPath)}`); + const escapedResourcePath = htmlentities(resourceUrl(link.resourceFullPath)); const escapedMime = htmlentities(resource.mime); if (options.videoPlayerEnabled && resource.mime.indexOf('video/') === 0) { diff --git a/packages/renderer/MdToHtml/rules/html_image.ts b/packages/renderer/MdToHtml/rules/html_image.ts index af5022f7ff..2b2a7201db 100644 --- a/packages/renderer/MdToHtml/rules/html_image.ts +++ b/packages/renderer/MdToHtml/rules/html_image.ts @@ -3,7 +3,7 @@ import htmlUtils from '../../htmlUtils'; import utils from '../../utils'; function renderImageHtml(before: string, src: string, after: string, ruleOptions: RuleOptions) { - const r = utils.imageReplacement(ruleOptions.ResourceModel, src, ruleOptions.resources, ruleOptions.resourceBaseUrl); + const r = utils.imageReplacement(ruleOptions.ResourceModel, src, ruleOptions.resources, ruleOptions.resourceBaseUrl, ruleOptions.itemIdToUrl); if (typeof r === 'string') return r; if (r) return ``; return `[Image: ${src}]`; diff --git a/packages/renderer/MdToHtml/rules/image.ts b/packages/renderer/MdToHtml/rules/image.ts index 2f181b8282..806dd9de84 100644 --- a/packages/renderer/MdToHtml/rules/image.ts +++ b/packages/renderer/MdToHtml/rules/image.ts @@ -14,7 +14,7 @@ function plugin(markdownIt: any, ruleOptions: RuleOptions) { if (!Resource.isResourceUrl(src) || ruleOptions.plainResourceRendering) return defaultRender(tokens, idx, options, env, self); - const r = utils.imageReplacement(ruleOptions.ResourceModel, src, ruleOptions.resources, ruleOptions.resourceBaseUrl); + const r = utils.imageReplacement(ruleOptions.ResourceModel, src, ruleOptions.resources, ruleOptions.resourceBaseUrl, ruleOptions.itemIdToUrl); if (typeof r === 'string') return r; if (r) { let js = ''; diff --git a/packages/renderer/MdToHtml/rules/link_open.ts b/packages/renderer/MdToHtml/rules/link_open.ts index 69ee610b8d..ce6b39553b 100644 --- a/packages/renderer/MdToHtml/rules/link_open.ts +++ b/packages/renderer/MdToHtml/rules/link_open.ts @@ -20,6 +20,7 @@ function plugin(markdownIt: any, ruleOptions: RuleOptions) { plainResourceRendering: ruleOptions.plainResourceRendering, postMessageSyntax: ruleOptions.postMessageSyntax, enableLongPress: ruleOptions.enableLongPress, + itemIdToUrl: ruleOptions.itemIdToUrl, }); ruleOptions.context.currentLinks.push({ diff --git a/packages/renderer/README.md b/packages/renderer/README.md index 405a79e98e..1f8486cd48 100644 --- a/packages/renderer/README.md +++ b/packages/renderer/README.md @@ -57,7 +57,7 @@ function loadPluginAssets(assets) { Whenever updating a Markdown-it plugin, such as Katex or Mermaid, make sure to run `npm run buildAssets`, which will compile the CSS and JS for use in the Joplin applications. -### Adding asset files +### Adding asset files A plugin (or rule) can have any number of assets, such as CSS or font files, associated with it. To add an asset to a plugin, follow these steps: diff --git a/packages/renderer/noteStyle.ts b/packages/renderer/noteStyle.ts index 520995785d..5b6b1851ba 100644 --- a/packages/renderer/noteStyle.ts +++ b/packages/renderer/noteStyle.ts @@ -126,10 +126,15 @@ export default function(theme: any) { margin-top: 0.2em; margin-bottom: 0; } + + a[data-resource-id] { + display: flex; + flex-direction: row; + align-items: center; + } + .resource-icon { display: inline-block; - position: relative; - top: .5em; text-decoration: none; width: 1.2em; height: 1.4em; diff --git a/packages/renderer/utils.ts b/packages/renderer/utils.ts index eb5de3f65a..f4c700480a 100644 --- a/packages/renderer/utils.ts +++ b/packages/renderer/utils.ts @@ -122,7 +122,9 @@ utils.resourceStatus = function(ResourceModel: any, resourceInfo: any) { return resourceStatus; }; -utils.imageReplacement = function(ResourceModel: any, src: string, resources: any, resourceBaseUrl: string) { +export type ItemIdToUrlHandler = (resource: any)=> string; + +utils.imageReplacement = function(ResourceModel: any, src: string, resources: any, resourceBaseUrl: string, itemIdToUrl: ItemIdToUrlHandler = null) { if (!ResourceModel || !resources) return null; if (!ResourceModel.isResourceUrl(src)) return null; @@ -136,12 +138,29 @@ utils.imageReplacement = function(ResourceModel: any, src: string, resources: an const icon = utils.resourceStatusImage(resourceStatus); return `
` + `` + '
'; } - const mime = resource.mime ? resource.mime.toLowerCase() : ''; if (ResourceModel.isSupportedImageMimeType(mime)) { - let newSrc = `./${ResourceModel.filename(resource)}`; - if (resourceBaseUrl) newSrc = resourceBaseUrl + newSrc; - newSrc += `?t=${resource.updated_time}`; + let newSrc = ''; + + if (itemIdToUrl) { + newSrc = itemIdToUrl(resource.id); + } else { + const temp = []; + + if (resourceBaseUrl) { + temp.push(resourceBaseUrl); + } else { + temp.push('./'); + } + + temp.push(ResourceModel.filename(resource)); + temp.push(`?t=${resource.updated_time}`); + + newSrc = temp.join(''); + } + + // let newSrc = `./${ResourceModel.filename(resource)}`; + // newSrc += `?t=${resource.updated_time}`; return { 'data-resource-id': resource.id, src: newSrc, diff --git a/packages/server/nodemon.json b/packages/server/nodemon.json index 0b26dae746..ff3c3f550c 100644 --- a/packages/server/nodemon.json +++ b/packages/server/nodemon.json @@ -1,4 +1,4 @@ { "verbose": true, - "watch": ["dist/"] + "watch": ["dist/", "../renderer", "../lib"] } \ No newline at end of file diff --git a/packages/server/package.json b/packages/server/package.json index b9e19fc79b..df369ec6cc 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -5,7 +5,7 @@ "scripts": { "start-dev": "nodemon --config nodemon.json dist/app.js --env dev", "start": "node dist/app.js", - "generate-types": "rm -f db-buildTypes.sqlite && npm run start -- --migrate-db --env buildTypes && node dist/tools/generate-types.js && rm -f db-buildTypes.sqlite", + "generateTypes": "rm -f db-buildTypes.sqlite && npm run start -- --migrate-db --env buildTypes && node dist/tools/generateTypes.js && rm -f db-buildTypes.sqlite", "tsc": "tsc --project tsconfig.json", "test": "jest", "test-ci": "npm run test", @@ -14,6 +14,7 @@ "dependencies": { "@fortawesome/fontawesome-free": "^5.15.1", "@joplin/lib": "^1.0.9", + "@joplin/renderer": "^1.7.4", "bcryptjs": "^2.4.3", "bulma": "^0.9.1", "bulma-prefers-dark": "^0.1.0-beta.0", diff --git a/packages/server/public/css/main.css b/packages/server/public/css/main.css index 6efa61786e..cc4c60c561 100644 --- a/packages/server/public/css/main.css +++ b/packages/server/public/css/main.css @@ -18,8 +18,19 @@ input.form-control { align-items: center; } +.navbar .logo-text { + font-size: 2.2em; + font-weight: bold; + margin-left: 0.5em; +} + +/* .navbar .logo { height: 50px; +} */ + +.navbar .navbar-item img { + max-height: 3em; } .main { diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts index 9363b66a62..25f48a6061 100644 --- a/packages/server/src/app.ts +++ b/packages/server/src/app.ts @@ -8,12 +8,12 @@ import Logger, { LoggerWrapper, TargetType } from '@joplin/lib/Logger'; import config, { initConfig, runningInDocker, EnvVariables } from './config'; import { createDb, dropDb } from './tools/dbTools'; import { dropTables, connectDb, disconnectDb, migrateDb, waitForConnection, sqliteFilePath } from './db'; -import modelFactory from './models/factory'; import { AppContext, Env } from './utils/types'; import FsDriverNode from '@joplin/lib/fs-driver-node'; import routeHandler from './middleware/routeHandler'; import notificationHandler from './middleware/notificationHandler'; import ownerHandler from './middleware/ownerHandler'; +import setupAppContext from './utils/setupAppContext'; const nodeEnvFile = require('node-env-file'); const { shimInit } = require('@joplin/lib/shim-init-node.js'); @@ -77,6 +77,8 @@ async function main() { }); await fs.mkdirp(config().logDir); + await fs.mkdirp(config().tempDir); + Logger.fsDriver_ = new FsDriverNode(); const globalLogger = new Logger(); // globalLogger.addTarget(TargetType.File, { path: `${config().logDir}/app.txt` }); @@ -114,8 +116,6 @@ async function main() { appLogger().info('DB Config:', markPasswords(config().database)); if (config().database.client === 'sqlite3') appLogger().info('DB file:', sqliteFilePath(config().database.name)); - const appContext = app.context as AppContext; - appLogger().info('Trying to connect to database...'); const connectionCheck = await waitForConnection(config().database); @@ -123,10 +123,9 @@ async function main() { delete connectionCheckLogInfo.connection; appLogger().info('Connection check:', connectionCheckLogInfo); - appContext.env = env; - appContext.db = connectionCheck.connection; - appContext.models = modelFactory(appContext.db, config().baseUrl); - appContext.appLogger = appLogger; + const appContext = app.context as AppContext; + + await setupAppContext(appContext, env, connectionCheck.connection, appLogger); appLogger().info('Migrating database...'); await migrateDb(appContext.db); diff --git a/packages/server/src/apps/joplin/Application.ts b/packages/server/src/apps/joplin/Application.ts new file mode 100644 index 0000000000..52174a40fa --- /dev/null +++ b/packages/server/src/apps/joplin/Application.ts @@ -0,0 +1,267 @@ +import JoplinDatabase from '@joplin/lib/JoplinDatabase'; +import Logger from '@joplin/lib/Logger'; +import BaseModel, { ModelType } from '@joplin/lib/BaseModel'; +import BaseItem from '@joplin/lib/models/BaseItem'; +import Note from '@joplin/lib/models/Note'; +import { File, Share, Uuid } from '../../db'; +import { NoteEntity } from '@joplin/lib/services/database/types'; +import { MarkupToHtml } from '@joplin/renderer'; +import Setting from '@joplin/lib/models/Setting'; +import Resource from '@joplin/lib/models/Resource'; +import FileModel from '../../models/FileModel'; +import { ErrorNotFound } from '../../utils/errors'; +import BaseApplication from '../../services/BaseApplication'; +import { formatDateTime } from '../../utils/time'; +const { DatabaseDriverNode } = require('@joplin/lib/database-driver-node.js'); +const { themeStyle } = require('@joplin/lib/theme'); + +const logger = Logger.create('JoplinApp'); + +export interface FileViewerResponse { + body: any; + mime: string; + size: number; +} + +interface ResourceInfo { + localState: any; + item: any; +} + +interface LinkedItemInfo { + item: any; + file: File; +} + +type LinkedItemInfos = Record; + +type ResourceInfos = Record; + +export default class Application extends BaseApplication { + + // Although we don't use the database to store data, we still need to setup + // so that its schema can be accessed. This is needed for example by + // Note.unserialize to know what fields are valid for a note, and to format + // the field values correctly. + private db_: JoplinDatabase; + + private pluginAssetRootDir_: string; + + public async initialize() { + this.mustache.prefersDarkEnabled = false; + this.pluginAssetRootDir_ = require('path').resolve(__dirname, '../../..', 'node_modules/@joplin/renderer/assets'); + + const filePath = `${this.config.tempDir}/joplin.sqlite`; + + this.db_ = new JoplinDatabase(new DatabaseDriverNode()); + this.db_.setLogger(logger as Logger); + await this.db_.open({ name: filePath }); + + BaseModel.setDb(this.db_); + + // Only load the classes that will be needed to render the notes and + // resources. + BaseItem.loadClass('Note', Note); + BaseItem.loadClass('Resource', Resource); + } + + public async localFileFromUrl(url: string): Promise { + const pluginAssetPrefix = 'apps/joplin/pluginAssets/'; + + if (url.indexOf(pluginAssetPrefix) === 0) { + return `${this.pluginAssetRootDir_}/${url.substr(pluginAssetPrefix.length)}`; + } + + return null; + } + + private itemIdFilename(itemId: string): string { + return `${itemId}.md`; + } + + private async itemMetadataFile(parentId: Uuid, itemId: string): Promise { + const file = await this.models.file().fileByName(parentId, this.itemIdFilename(itemId), { skipPermissionCheck: true }); + return this.models.file().loadWithContent(file.id, { skipPermissionCheck: true }); + } + + private async unserializeItem(file: File): Promise { + const content = file.content.toString(); + return BaseItem.unserialize(content); + } + + private async resourceInfos(linkedItemInfos: LinkedItemInfos): Promise { + const output: Record = {}; + + for (const itemId of Object.keys(linkedItemInfos)) { + const info = linkedItemInfos[itemId]; + + if (info.item.type_ !== ModelType.Resource) continue; + + output[info.item.id] = { + item: info.item, + localState: { + fetch_status: Resource.FETCH_STATUS_DONE, + }, + }; + } + + return output; + } + + private async noteLinkedItemInfos(noteFileParentId: string, note: NoteEntity): Promise { + const itemIds = await Note.linkedItemIds(note.body); + const output: LinkedItemInfos = {}; + + for (const itemId of itemIds) { + const itemFile = await this.itemMetadataFile(noteFileParentId, itemId); + output[itemId] = { + item: await this.unserializeItem(itemFile), + file: itemFile, + }; + } + + return output; + } + + private async resourceDir(fileModel: FileModel, parentId: Uuid): Promise { + const parent = await fileModel.load(parentId); + const parentFullPath = await fileModel.itemFullPath(parent); + const dirPath = fileModel.resolve(parentFullPath, '.resource'); + return fileModel.pathToFile(dirPath); + } + + private async itemFile(fileModel: FileModel, parentId: Uuid, itemType: ModelType, itemId: string): Promise { + let output: File = null; + + if (itemType === ModelType.Resource) { + const resourceDir = await this.resourceDir(fileModel, parentId); + output = await fileModel.fileByName(resourceDir.id, itemId); + } else if (itemType === ModelType.Note) { + output = await fileModel.fileByName(parentId, this.itemIdFilename(itemId)); + } else { + throw new Error(`Unsupported type: ${itemType}`); + } + + return fileModel.loadWithContent(output.id); + } + + private async renderResource(file: File): Promise { + return { + body: file.content, + mime: file.mime_type, + size: file.size, + }; + } + + private async renderNote(share: Share, note: NoteEntity, resourceInfos: ResourceInfos, linkedItemInfos: LinkedItemInfos): Promise { + const markupToHtml = new MarkupToHtml({ + ResourceModel: Resource, + }); + + const renderOptions: any = { + resources: resourceInfos, + + itemIdToUrl: (itemId: Uuid) => { + const item = linkedItemInfos[itemId].item; + if (!item) throw new Error(`No such item in this note: ${itemId}`); + + if (item.type_ === ModelType.Note) { + return '#'; + } else if (item.type_ === ModelType.Resource) { + return `${this.models.share().shareUrl(share.id)}?resource_id=${item.id}&t=${item.updated_time}`; + } else { + throw new Error(`Unsupported item type: ${item.type_}`); + } + }, + + // Switch-off the media players because there's no option to toggle + // them on and off. + audioPlayerEnabled: false, + videoPlayerEnabled: false, + pdfViewerEnabled: false, + }; + + const result = await markupToHtml.render(note.markup_language, note.body, themeStyle(Setting.THEME_LIGHT), renderOptions); + + const bodyHtml = await this.mustache.renderView({ + cssFiles: ['note'], + jsFiles: ['note'], + name: 'note', + path: 'note', + content: { + note: { + ...note, + bodyHtml: result.html, + updatedDateTime: formatDateTime(note.updated_time), + }, + cssStrings: result.cssStrings.join('\n'), + assetsJs: ` + const joplinNoteViewer = { + pluginAssets: ${JSON.stringify(result.pluginAssets)}, + appBaseUrl: ${JSON.stringify(this.appBaseUrl)}, + }; + `, + }, + }); + + return { + body: bodyHtml, + mime: 'text/html', + size: bodyHtml.length, + }; + } + + public async renderFile(file: File, share: Share, query: Record): Promise { + const fileModel = this.models.file({ userId: file.owner_id }); + + const rootNote: NoteEntity = await this.unserializeItem(file); + const linkedItemInfos = await this.noteLinkedItemInfos(file.parent_id, rootNote); + const resourceInfos = await this.resourceInfos(linkedItemInfos); + + const fileToRender = { + file: file, + itemId: rootNote.id, + }; + + if (query.resource_id) { + fileToRender.file = await this.itemFile(fileModel, file.parent_id, ModelType.Resource, query.resource_id); + fileToRender.itemId = query.resource_id; + } + + // No longer supported - need to decide what to do about note links. + + // if (query.note_id) { + // fileToRender.file = await this.itemFile(fileModel, file.parent_id, ModelType.Note, query.note_id); + // fileToRender.itemId = query.note_id; + // } + + if (fileToRender.file !== file && !linkedItemInfos[fileToRender.itemId]) { + throw new ErrorNotFound(`Item "${fileToRender.itemId}" does not belong to this note`); + } + + const itemToRender = fileToRender.file === file ? rootNote : linkedItemInfos[fileToRender.itemId].item; + const itemType: ModelType = itemToRender.type_; + + if (itemType === ModelType.Resource) { + return this.renderResource(fileToRender.file); + } else if (itemType === ModelType.Note) { + return this.renderNote(share, itemToRender, resourceInfos, linkedItemInfos); + } else { + throw new Error(`Cannot render item with type "${itemType}"`); + } + } + + public async isItemFile(file: File): Promise { + if (file.mime_type !== 'text/markdown') return false; + + try { + await this.unserializeItem(file); + } catch (error) { + // No need to log - it means it's not a note file + return false; + } + + return true; + } + +} diff --git a/packages/server/src/apps/joplin/css/note.css b/packages/server/src/apps/joplin/css/note.css new file mode 100644 index 0000000000..841c7bc41f --- /dev/null +++ b/packages/server/src/apps/joplin/css/note.css @@ -0,0 +1,28 @@ +.main { + padding-left: 0; + padding-right: 0; +} + +.navbar { + padding: .5em; + border-bottom: 1px solid #dddddd; + box-shadow: 0px 2px 8px #cccccc; +} + +.page-note .note-main { + margin-top: 2em; + max-width: 840px; + margin-left: auto; + margin-right: auto; +} + +.page-note .last-updated { + font-size: 1.1em; + opacity: 0.6; +} + +.page-note h1.title { + font-size: 2.2em; + margin-top: 0.4em; + border-bottom: none; +} diff --git a/packages/server/src/apps/joplin/js/note.js b/packages/server/src/apps/joplin/js/note.js new file mode 100644 index 0000000000..ac7fe1d49a --- /dev/null +++ b/packages/server/src/apps/joplin/js/note.js @@ -0,0 +1,44 @@ +/* global joplinNoteViewer */ + +function addPluginAssets(appBaseUrl, assets) { + if (!assets) return; + + const pluginAssetsContainer = document.getElementById('joplin-container-pluginAssetsContainer'); + + for (let i = 0; i < assets.length; i++) { + const asset = assets[i]; + + if (asset.mime === 'application/javascript') { + const script = document.createElement('script'); + script.src = `${appBaseUrl}/${asset.path}`; + pluginAssetsContainer.appendChild(script); + } else if (asset.mime === 'text/css') { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = `${appBaseUrl}/${asset.path}`; + pluginAssetsContainer.appendChild(link); + } + } +} + +function docReady(fn) { + if (document.readyState === 'complete' || document.readyState === 'interactive') { + setTimeout(fn, 1); + } else { + document.addEventListener('DOMContentLoaded', fn); + } +} + +docReady(() => { + addPluginAssets(joplinNoteViewer.appBaseUrl, joplinNoteViewer.pluginAssets); + + document.addEventListener('click', event => { + const element = event.target; + + // Detects if it's a note link and, if so, display a message + if (element && element.getAttribute('href') === '#' && element.getAttribute('data-resource-id')) { + event.preventDefault(); + alert('This note has not been shared'); + } + }); +}); diff --git a/packages/server/src/apps/joplin/views/note.mustache b/packages/server/src/apps/joplin/views/note.mustache new file mode 100644 index 0000000000..81a045c2d4 --- /dev/null +++ b/packages/server/src/apps/joplin/views/note.mustache @@ -0,0 +1,19 @@ + + + + +
+ +
+ +
+

Last updated: {{note.updatedDateTime}}

+

{{note.title}}

+ {{{note.bodyHtml}}} +
diff --git a/packages/server/src/config.ts b/packages/server/src/config.ts index 35a1fa8528..13e9460014 100644 --- a/packages/server/src/config.ts +++ b/packages/server/src/config.ts @@ -78,6 +78,7 @@ export function initConfig(env: EnvVariables) { rootDir: rootDir, viewDir: viewDir, layoutDir: `${viewDir}/layouts`, + tempDir: `${rootDir}/temp`, logDir: `${rootDir}/logs`, database: databaseConfigFromEnv(runningInDocker_, env), port: appPort, diff --git a/packages/server/src/db.ts b/packages/server/src/db.ts index 9fcc19ab40..c226f2ba9d 100644 --- a/packages/server/src/db.ts +++ b/packages/server/src/db.ts @@ -211,6 +211,11 @@ export enum ChangeType { Delete = 3, } +export enum ShareType { + Link = 1, // When a note is shared via a public link + App = 2, // When a note is shared with another user on the same server instance +} + export interface WithDates { updated_time?: number; created_time?: number; @@ -289,6 +294,12 @@ export interface Notification extends WithDates, WithUuid { canBeDismissed?: number; } +export interface Share extends WithDates, WithUuid { + owner_id?: Uuid; + file_id?: Uuid; + type?: ShareType; +} + export const databaseSchema: DatabaseTables = { users: { id: { type: 'string' }, @@ -359,5 +370,13 @@ export const databaseSchema: DatabaseTables = { updated_time: { type: 'string' }, created_time: { type: 'string' }, }, + shares: { + id: { type: 'string' }, + owner_id: { type: 'string' }, + file_id: { type: 'string' }, + type: { type: 'number' }, + updated_time: { type: 'string' }, + created_time: { type: 'string' }, + }, }; // AUTO-GENERATED-TYPES diff --git a/packages/server/src/middleware/routeHandler.ts b/packages/server/src/middleware/routeHandler.ts index 126c37a578..5238f4b3b5 100644 --- a/packages/server/src/middleware/routeHandler.ts +++ b/packages/server/src/middleware/routeHandler.ts @@ -2,7 +2,16 @@ import routes from '../routes/routes'; import { ErrorForbidden, ErrorNotFound } from '../utils/errors'; import { routeResponseFormat, findMatchingRoute, Response, RouteResponseFormat, MatchedRoute } from '../utils/routeUtils'; import { AppContext, Env, HttpMethod } from '../utils/types'; -import mustacheService, { isView, View } from '../services/MustacheService'; +import MustacheService, { isView, View } from '../services/MustacheService'; +import config from '../config'; + +let mustache_: MustacheService = null; +function mustache(): MustacheService { + if (!mustache_) { + mustache_ = new MustacheService(config().viewDir, config().baseUrl); + } + return mustache_; +} export default async function(ctx: AppContext) { ctx.appLogger().info(`${ctx.request.method} ${ctx.path}`); @@ -28,7 +37,7 @@ export default async function(ctx: AppContext) { ctx.response = responseObject.response; } else if (isView(responseObject)) { ctx.response.status = 200; - ctx.response.body = await mustacheService.renderView(responseObject, { + ctx.response.body = await mustache().renderView(responseObject, { notifications: ctx.notifications || [], hasNotifications: !!ctx.notifications && !!ctx.notifications.length, owner: ctx.owner, @@ -61,7 +70,7 @@ export default async function(ctx: AppContext) { stack: ctx.env === Env.Dev ? error.stack : '', }, }; - ctx.response.body = await mustacheService.renderView(view); + ctx.response.body = await mustache().renderView(view); } else { // JSON ctx.response.set('Content-Type', 'application/json'); const r: any = { error: error.message }; diff --git a/packages/server/src/migrations/20203012152842_shares.ts b/packages/server/src/migrations/20203012152842_shares.ts new file mode 100644 index 0000000000..42a5309c02 --- /dev/null +++ b/packages/server/src/migrations/20203012152842_shares.ts @@ -0,0 +1,17 @@ +import * as Knex from 'knex'; +import { DbConnection } from '../db'; + +export async function up(db: DbConnection): Promise { + await db.schema.createTable('shares', function(table: Knex.CreateTableBuilder) { + table.string('id', 32).unique().primary().notNullable(); + table.string('owner_id', 32).notNullable(); + table.string('file_id', 32).notNullable(); + table.integer('type').notNullable(); + table.bigInteger('updated_time').notNullable(); + table.bigInteger('created_time').notNullable(); + }); +} + +export async function down(db: DbConnection): Promise { + await db.schema.dropTable('shares'); +} diff --git a/packages/server/src/models/ApiClientModel.ts b/packages/server/src/models/ApiClientModel.ts index df794d68e3..36f25aafa1 100644 --- a/packages/server/src/models/ApiClientModel.ts +++ b/packages/server/src/models/ApiClientModel.ts @@ -1,6 +1,7 @@ import BaseModel from './BaseModel'; +import { ApiClient } from '../db'; -export default class ApiClientModel extends BaseModel { +export default class ApiClientModel extends BaseModel { protected get tableName(): string { return 'api_clients'; diff --git a/packages/server/src/models/BaseModel.ts b/packages/server/src/models/BaseModel.ts index 4c8b157371..24cbcd798c 100644 --- a/packages/server/src/models/BaseModel.ts +++ b/packages/server/src/models/BaseModel.ts @@ -1,12 +1,9 @@ -import { WithDates, WithUuid, File, User, Session, Permission, databaseSchema, ApiClient, DbConnection, Change, ItemType, ChangeType, Notification } from '../db'; +import { WithDates, WithUuid, databaseSchema, DbConnection, ItemType, ChangeType } from '../db'; import TransactionHandler from '../utils/TransactionHandler'; import uuidgen from '../utils/uuidgen'; import { ErrorUnprocessableEntity, ErrorBadRequest } from '../utils/errors'; import { Models } from './factory'; -export type AnyItemType = File | User | Session | Permission | ApiClient | Change | Notification; -export type AnyItemTypes = File[] | User[] | Session[] | Permission[] | ApiClient[] | Change[] | Notification[]; - export interface ModelOptions { userId?: string; } @@ -27,7 +24,7 @@ export interface ValidateOptions { rules?: any; } -export default abstract class BaseModel { +export default abstract class BaseModel { private options_: ModelOptions = null; private defaultFields_: string[] = []; @@ -156,52 +153,61 @@ export default abstract class BaseModel { await this.transactionHandler_.commit(txIndex); } - public async all(): Promise { + public async all(): Promise { return this.db(this.tableName).select(...this.defaultFields); } - public fromApiInput(object: AnyItemType): AnyItemType { - return object; + public fromApiInput(object: T): T { + const blackList = ['updated_time', 'created_time', 'owner_id']; + const whiteList = Object.keys(databaseSchema[this.tableName]); + const output: any = { ...object }; + + for (const f in object) { + if (blackList.includes(f)) delete output[f]; + if (!whiteList.includes(f)) delete output[f]; + } + + return output; } public toApiOutput(object: any): any { return { ...object }; } - protected async validate(object: AnyItemType, options: ValidateOptions = {}): Promise { + protected async validate(object: T, options: ValidateOptions = {}): Promise { if (!options.isNew && !(object as WithUuid).id) throw new ErrorUnprocessableEntity('id is missing'); return object; } - protected async isNew(object: AnyItemType, options: SaveOptions): Promise { + protected async isNew(object: T, options: SaveOptions): Promise { if (options.isNew === false) return false; if (options.isNew === true) return true; return !(object as WithUuid).id; } - private async handleChangeTracking(options: SaveOptions, item: AnyItemType, changeType: ChangeType): Promise { + private async handleChangeTracking(options: SaveOptions, item: T, changeType: ChangeType): Promise { const trackChanges = this.trackChanges && options.trackChanges !== false; if (!trackChanges) return; let parentId = null; if (this.hasParentId) { if (!('parent_id' in item)) { - const temp: any = await this.db(this.tableName).select(['parent_id']).where('id', '=', item.id).first(); + const temp: any = await this.db(this.tableName).select(['parent_id']).where('id', '=', (item as WithUuid).id).first(); parentId = temp.parent_id; } else { - parentId = item.parent_id; + parentId = (item as any).parent_id; } } // Sanity check - shouldn't happen // Parent ID can be an empty string for root folders, but it shouldn't be null or undefined - if (this.hasParentId && !parentId && parentId !== '') throw new Error(`Could not find parent ID for item: ${item.id}`); + if (this.hasParentId && !parentId && parentId !== '') throw new Error(`Could not find parent ID for item: ${(item as WithUuid).id}`); const changeModel = this.models().change({ userId: this.userId }); await changeModel.add(this.itemType, parentId, (item as WithUuid).id, (item as any).name || '', changeType); } - public async save(object: AnyItemType, options: SaveOptions = {}): Promise { + public async save(object: T, options: SaveOptions = {}): Promise { if (!object) throw new Error('Object cannot be empty'); const toSave = Object.assign({}, object); @@ -231,7 +237,7 @@ export default abstract class BaseModel { if (!objectId) throw new Error('Missing "id" property'); delete (toSave as WithUuid).id; const updatedCount: number = await this.db(this.tableName).update(toSave).where({ id: objectId }); - toSave.id = objectId; + (toSave as WithUuid).id = objectId; await this.handleChangeTracking(options, toSave, ChangeType.Update); @@ -243,12 +249,12 @@ export default abstract class BaseModel { return toSave; } - public async loadByIds(ids: string[]): Promise { + public async loadByIds(ids: string[]): Promise { if (!ids.length) return []; return this.db(this.tableName).select(this.defaultFields).whereIn('id', ids); } - public async load(id: string): Promise { + public async load(id: string): Promise { if (!id) throw new Error('id cannot be empty'); return this.db(this.tableName).select(this.defaultFields).where({ id: id }).first(); @@ -263,7 +269,7 @@ export default abstract class BaseModel { const trackChanges = this.trackChanges; - let itemsWithParentIds: AnyItemType[] = null; + let itemsWithParentIds: T[] = null; if (trackChanges) { itemsWithParentIds = await this.db(this.tableName).select(['id', 'parent_id', 'name']).whereIn('id', ids); } diff --git a/packages/server/src/models/ChangeModel.ts b/packages/server/src/models/ChangeModel.ts index 783936b859..71bfb5f913 100644 --- a/packages/server/src/models/ChangeModel.ts +++ b/packages/server/src/models/ChangeModel.ts @@ -24,7 +24,7 @@ export function defaultChangePagination(): ChangePagination { }; } -export default class ChangeModel extends BaseModel { +export default class ChangeModel extends BaseModel { public get tableName(): string { return 'changes'; @@ -44,7 +44,7 @@ export default class ChangeModel extends BaseModel { owner_id: this.userId, }; - return this.save(change); + return this.save(change) as Change; } // Note: doesn't currently support checking for changes recursively but this @@ -58,7 +58,7 @@ export default class ChangeModel extends BaseModel { let changeAtCursor: Change = null; if (pagination.cursor) { - changeAtCursor = await this.load(pagination.cursor); + changeAtCursor = await this.load(pagination.cursor) as Change; if (!changeAtCursor) throw new ErrorResyncRequired(); } diff --git a/packages/server/src/models/FileModel.test.ts b/packages/server/src/models/FileModel.test.ts index d080b0f21d..4b90db72ee 100644 --- a/packages/server/src/models/FileModel.test.ts +++ b/packages/server/src/models/FileModel.test.ts @@ -43,14 +43,51 @@ describe('FileModel', function() { for (const t of testCases) { const file: File = await fileModel.loadByName(t); const path = await fileModel.itemFullPath(file); - const fileBack: File = await fileModel.entityFromItemId(path); - expect(file.id).toBe(fileBack.id); + const fileBackId: string = await fileModel.pathToFileId(path); + expect(file.id).toBe(fileBackId); } const rootPath = await fileModel.itemFullPath(await fileModel.userRootFile()); expect(rootPath).toBe('root'); - const fileBack: File = await fileModel.entityFromItemId(rootPath); - expect(fileBack.id).toBe(rootId); + const fileBackId: string = await fileModel.pathToFileId(rootPath); + expect(fileBackId).toBe(rootId); + }); + + test('should resolve file paths', async function() { + const testCases = [ + [ + ['root', '.resource', 'test'], + 'root:/.resource/test:', + ], + [ + ['root:/.resource:', 'test'], + 'root:/.resource/test:', + ], + [ + ['root:/.resource:', ''], + 'root:/.resource:', + ], + [ + ['root:/.resource:'], + 'root:/.resource:', + ], + [ + ['root:/.resource:'], + 'root:/.resource:', + ], + [ + ['root'], + 'root', + ], + ]; + + const fileModel = models().file(); + + for (const t of testCases) { + const [input, expected] = t; + const actual = fileModel.resolve(...input); + expect(actual).toBe(expected); + } }); }); diff --git a/packages/server/src/models/FileModel.ts b/packages/server/src/models/FileModel.ts index 486833b234..f8e4e56f2d 100644 --- a/packages/server/src/models/FileModel.ts +++ b/packages/server/src/models/FileModel.ts @@ -1,5 +1,5 @@ import BaseModel, { ValidateOptions, SaveOptions, DeleteOptions } from './BaseModel'; -import { File, ItemType, databaseSchema } from '../db'; +import { File, ItemType, databaseSchema, Uuid } from '../db'; import { ErrorForbidden, ErrorUnprocessableEntity, ErrorNotFound, ErrorBadRequest, ErrorConflict } from '../utils/errors'; import uuidgen from '../utils/uuidgen'; import { splitItemPath, filePathInfo } from '../utils/routeUtils'; @@ -10,11 +10,13 @@ const mimeUtils = require('@joplin/lib/mime-utils.js').mime; const nodeEnv = process.env.NODE_ENV || 'development'; +const removeTrailingColonsRegex = /^(:|)(.*?)(:|)$/; + export interface PaginatedFiles extends PaginatedResults { items: File[]; } -export interface EntityFromItemIdOptions { +export interface PathToFileOptions { mustExist?: boolean; returnFullEntity?: boolean; } @@ -23,7 +25,7 @@ export interface LoadOptions { skipPermissionCheck?: boolean; } -export default class FileModel extends BaseModel { +export default class FileModel extends BaseModel { private readonly reservedCharacters = ['/', '\\', '*', '<', '>', '?', ':', '|', '#', '%']; @@ -57,6 +59,10 @@ export default class FileModel extends BaseModel { return r ? r.id : ''; } + private isSpecialDir(dirname: string): boolean { + return dirname === 'root'; + } + private async specialDirId(dirname: string): Promise { if (dirname === 'root') return this.userRootFileId(); return null; // Not a special dir @@ -92,28 +98,75 @@ export default class FileModel extends BaseModel { return output; } - public async itemFullPath(item: File): Promise { + public async itemFullPath(item: File, loadOptions: LoadOptions = {}): Promise { const segments: string[] = []; while (item) { if (item.is_root) break; segments.splice(0, 0, item.name); - item = item.parent_id ? await this.load(item.parent_id) : null; + item = item.parent_id ? await this.load(item.parent_id, loadOptions) : null; } return segments.length ? (`root:/${segments.join('/')}:`) : 'root'; } - public async entityFromItemId(idOrPath: string, options: EntityFromItemIdOptions = {}): Promise { + // Remove first and last colon from a path element + private removeTrailingColons(path: string): string { + return path.replace(removeTrailingColonsRegex, '$2'); + } + + public resolve(...paths: string[]): string { + if (!paths.length) throw new Error('Path is empty'); + + let pathElements: string[] = []; + + for (const p of paths) { + pathElements = pathElements.concat(p.split('/').map(s => this.removeTrailingColons(s))); + } + + pathElements = pathElements.filter(p => !!p); + + if (!this.isSpecialDir(pathElements[0])) throw new Error(`Path must start with a special dir: ${pathElements.join('/')}`); + + if (pathElements.length === 1) return pathElements[0]; + + // If the output is just "root", we return that only. Otherwise we build the path, eg. `root:/.resource/file:'` + const specialDir = pathElements.splice(0, 1)[0]; + + return `${specialDir}:/${pathElements.join('/')}:`; + } + + // Same as `pathToFile` but returns the ID only. + public async pathToFileId(idOrPath: string, options: PathToFileOptions = {}): Promise { + const file = await this.pathToFile(idOrPath, { + ...options, + returnFullEntity: false, + }); + return file ? file.id : null; + } + + // Converts an ID such as "Ps2YtQ8Udi4eCYm1A5bLFDGhHCWWCR43" or a path such + // as "root:/path/to/file.txt:" to the actual file. + public async pathToFile(idOrPath: string, options: PathToFileOptions = {}): Promise { if (!idOrPath) throw new Error('ID cannot be null'); - options = { mustExist: true, ...options }; + options = { + mustExist: true, + returnFullEntity: true, + ...options, + }; const specialDirId = await this.specialDirId(idOrPath); if (specialDirId) { return options.returnFullEntity ? this.load(specialDirId) : { id: specialDirId }; } else if (idOrPath.indexOf(':') < 0) { - return options.returnFullEntity ? this.load(idOrPath) : { id: idOrPath }; + if (options.mustExist) { + const file = await this.load(idOrPath); + if (!file) throw new ErrorNotFound(`file not found: ${idOrPath}`); + return options.returnFullEntity ? file : { id: file.id }; + } else { + return options.returnFullEntity ? this.load(idOrPath) : { id: idOrPath }; + } } else { // When this input is a path, there can be two cases: // - A path to an existing file - in which case we return the file @@ -153,11 +206,17 @@ export default class FileModel extends BaseModel { return Object.keys(databaseSchema[this.tableName]).filter(f => f !== 'content'); } - private async fileByName(parentId: string, name: string): Promise { - return this.db(this.tableName).select(...this.defaultFields).where({ + public async fileByName(parentId: string, name: string, options: LoadOptions = {}): Promise { + const file = await this.db(this.tableName).select(...this.defaultFields).where({ parent_id: parentId, name: name, }).first(); + + if (!file) return null; + + if (!options.skipPermissionCheck) await this.checkCanReadPermissions(file); + + return file; } protected async validate(object: File, options: ValidateOptions = {}): Promise { @@ -287,7 +346,7 @@ export default class FileModel extends BaseModel { } public async fileUrl(idOrPath: string, query: any = null): Promise { - const file: File = await this.entityFromItemId(idOrPath, { returnFullEntity: true }); + const file: File = await this.pathToFile(idOrPath); return setQueryParameters(`${this.baseUrl}/files/${await this.itemFullPath(file)}`, query); } diff --git a/packages/server/src/models/NotificationModel.ts b/packages/server/src/models/NotificationModel.ts index 237542d525..5bc05040b6 100644 --- a/packages/server/src/models/NotificationModel.ts +++ b/packages/server/src/models/NotificationModel.ts @@ -1,7 +1,7 @@ import { Notification, NotificationLevel, Uuid } from '../db'; import BaseModel from './BaseModel'; -export default class NotificationModel extends BaseModel { +export default class NotificationModel extends BaseModel { protected get tableName(): string { return 'notifications'; diff --git a/packages/server/src/models/PermissionModel.ts b/packages/server/src/models/PermissionModel.ts index 7326ada094..bdd67d54a1 100644 --- a/packages/server/src/models/PermissionModel.ts +++ b/packages/server/src/models/PermissionModel.ts @@ -12,7 +12,7 @@ export type PermissionGrantedMap = Record; export type PermissionMap = Record; -export default class PermissionModel extends BaseModel { +export default class PermissionModel extends BaseModel { protected get tableName(): string { return 'permissions'; diff --git a/packages/server/src/models/SessionModel.ts b/packages/server/src/models/SessionModel.ts index da1486b848..e1ffca109e 100644 --- a/packages/server/src/models/SessionModel.ts +++ b/packages/server/src/models/SessionModel.ts @@ -3,7 +3,7 @@ import { User, Session } from '../db'; import uuidgen from '../utils/uuidgen'; import { ErrorForbidden } from '../utils/errors'; -export default class SessionModel extends BaseModel { +export default class SessionModel extends BaseModel { protected get tableName(): string { return 'sessions'; diff --git a/packages/server/src/models/ShareModel.test.ts b/packages/server/src/models/ShareModel.test.ts new file mode 100644 index 0000000000..bfc3eabba3 --- /dev/null +++ b/packages/server/src/models/ShareModel.test.ts @@ -0,0 +1,36 @@ +import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, models, checkThrowAsync } from '../utils/testing/testUtils'; +import { ShareType } from '../db'; +import { ErrorBadRequest, ErrorNotFound } from '../utils/errors'; + +describe('ShareModel', function() { + + beforeAll(async () => { + await beforeAllDb('ShareModel'); + }); + + afterAll(async () => { + await afterAllTests(); + }); + + beforeEach(async () => { + await beforeEachDb(); + }); + + test('should validate share objects', async function() { + const { user } = await createUserAndSession(1, true); + const fileModel = models().file({ userId: user.id }); + const file = await fileModel.save({ + name: 'test', + parent_id: await fileModel.userRootFileId(), + }); + + let error = null; + + error = await checkThrowAsync(async () => await models().share({ userId: user.id }).add(20 as ShareType, file.id)); + expect(error instanceof ErrorBadRequest).toBe(true); + + error = await checkThrowAsync(async () => await models().share({ userId: user.id }).add(ShareType.Link, 'doesntexist')); + expect(error instanceof ErrorNotFound).toBe(true); + }); + +}); diff --git a/packages/server/src/models/ShareModel.ts b/packages/server/src/models/ShareModel.ts new file mode 100644 index 0000000000..be096ef9ab --- /dev/null +++ b/packages/server/src/models/ShareModel.ts @@ -0,0 +1,34 @@ +import { Share, ShareType, Uuid } from '../db'; +import { ErrorBadRequest } from '../utils/errors'; +import { setQueryParameters } from '../utils/urlUtils'; +import BaseModel, { ValidateOptions } from './BaseModel'; + +export default class ShareModel extends BaseModel { + + public get tableName(): string { + return 'shares'; + } + + protected async validate(share: Share, _options: ValidateOptions = {}): Promise { + if ('type' in share && ![ShareType.Link, ShareType.App].includes(share.type)) throw new ErrorBadRequest(`Invalid share type: ${share.type}`); + + return share; + } + + public async add(type: ShareType, path: string): Promise { + const fileId: Uuid = await this.models().file({ userId: this.userId }).pathToFileId(path); + + const toSave: Share = { + type: type, + file_id: fileId, + owner_id: this.userId, + }; + + return this.save(toSave); + } + + public shareUrl(id: Uuid, query: any = null): string { + return setQueryParameters(`${this.baseUrl}/shares/${id}`, query); + } + +} diff --git a/packages/server/src/models/UserModel.ts b/packages/server/src/models/UserModel.ts index 006e1efea7..0c62d3ed82 100644 --- a/packages/server/src/models/UserModel.ts +++ b/packages/server/src/models/UserModel.ts @@ -3,7 +3,7 @@ import { User } from '../db'; import * as auth from '../utils/auth'; import { ErrorUnprocessableEntity, ErrorForbidden } from '../utils/errors'; -export default class UserModel extends BaseModel { +export default class UserModel extends BaseModel { public get tableName(): string { return 'users'; diff --git a/packages/server/src/models/factory.ts b/packages/server/src/models/factory.ts index 0eb0ce4a72..ec567bb27c 100644 --- a/packages/server/src/models/factory.ts +++ b/packages/server/src/models/factory.ts @@ -63,6 +63,7 @@ import PermissionModel from './PermissionModel'; import SessionModel from './SessionModel'; import ChangeModel from './ChangeModel'; import NotificationModel from './NotificationModel'; +import ShareModel from './ShareModel'; export class Models { @@ -102,6 +103,10 @@ export class Models { return new NotificationModel(this.db_, newModelFactory, this.baseUrl_, options); } + public share(options: ModelOptions = null) { + return new ShareModel(this.db_, newModelFactory, this.baseUrl_, options); + } + } export default function newModelFactory(db: DbConnection, baseUrl: string): Models { diff --git a/packages/server/src/routes/api/files.test.ts b/packages/server/src/routes/api/files.test.ts index bece2e5ba9..f2dbdf628f 100644 --- a/packages/server/src/routes/api/files.test.ts +++ b/packages/server/src/routes/api/files.test.ts @@ -1,16 +1,11 @@ import { testAssetDir, beforeAllDb, randomHash, afterAllTests, beforeEachDb, createUserAndSession, models, tempDir } from '../../utils/testing/testUtils'; -import { getFileMetadataContext, getFileMetadata, deleteFileContent, deleteFileContext, deleteFile, postDirectoryContext, postDirectory, getDirectoryChildren, putFileContentContext, putFileContent, getFileContent, patchFileContext, patchFile, getDelta } from '../../utils/testing/apiUtils'; +import { testFilePath, getFileMetadataContext, getFileMetadata, deleteFileContent, deleteFileContext, deleteFile, postDirectoryContext, postDirectory, getDirectoryChildren, putFileContentContext, putFileContent, getFileContent, patchFileContext, patchFile, getDelta } from '../../utils/testing/fileApiUtils'; import * as fs from 'fs-extra'; import { ChangeType, File } from '../../db'; import { Pagination, PaginationOrderDir } from '../../models/utils/pagination'; import { ErrorUnprocessableEntity, ErrorForbidden, ErrorNotFound, ErrorConflict } from '../../utils/errors'; import { msleep } from '../../utils/time'; -function testFilePath(ext: string = 'jpg') { - const basename = ext === 'jpg' ? 'photo' : 'poster'; - return `${testAssetDir}/${basename}.${ext}`; -} - async function makeTempFileWithContent(content: string): Promise { const d = await tempDir(); const filePath = `${d}/${randomHash()}`; diff --git a/packages/server/src/routes/api/files.ts b/packages/server/src/routes/api/files.ts index f38165ca44..3940741a29 100644 --- a/packages/server/src/routes/api/files.ts +++ b/packages/server/src/routes/api/files.ts @@ -1,5 +1,5 @@ import { ErrorNotFound } from '../../utils/errors'; -import { File } from '../../db'; +import { File, Uuid } from '../../db'; import { bodyFields, formParse } from '../../utils/requestUtils'; import { SubPath, respondWithFileContent } from '../../utils/routeUtils'; import Router from '../../utils/Router'; @@ -11,30 +11,27 @@ const router = new Router(); router.get('api/files/:id', async (path: SubPath, ctx: AppContext) => { const fileModel = ctx.models.file({ userId: ctx.owner.id }); - const fileId = path.id; - const file: File = await fileModel.entityFromItemId(fileId); - const loadedFile = await fileModel.load(file.id); - if (!loadedFile) throw new ErrorNotFound(); - return fileModel.toApiOutput(loadedFile); + const file: File = await fileModel.pathToFile(path.id); + return fileModel.toApiOutput(file); }); router.patch('api/files/:id', async (path: SubPath, ctx: AppContext) => { const fileModel = ctx.models.file({ userId: ctx.owner.id }); const fileId = path.id; const inputFile: File = await bodyFields(ctx.req); - const existingFile: File = await fileModel.entityFromItemId(fileId); + const existingFileId: Uuid = await fileModel.pathToFileId(fileId); const newFile = fileModel.fromApiInput(inputFile); - newFile.id = existingFile.id; + newFile.id = existingFileId; return fileModel.toApiOutput(await fileModel.save(newFile)); }); router.del('api/files/:id', async (path: SubPath, ctx: AppContext) => { const fileModel = ctx.models.file({ userId: ctx.owner.id }); - const fileId = path.id; + // const fileId = path.id; try { - const file: File = await fileModel.entityFromItemId(fileId, { mustExist: false }); - if (!file.id) return; - await fileModel.delete(file.id); + const fileId: Uuid = await fileModel.pathToFileId(path.id, { mustExist: false }); + if (!fileId) return; + await fileModel.delete(fileId); } catch (error) { if (error instanceof ErrorNotFound) { // That's ok - a no-op @@ -46,9 +43,8 @@ router.del('api/files/:id', async (path: SubPath, ctx: AppContext) => { router.get('api/files/:id/content', async (path: SubPath, ctx: AppContext) => { const fileModel = ctx.models.file({ userId: ctx.owner.id }); - const fileId = path.id; - let file: File = await fileModel.entityFromItemId(fileId); - file = await fileModel.loadWithContent(file.id); + const fileId: Uuid = await fileModel.pathToFileId(path.id); + const file = await fileModel.loadWithContent(fileId); if (!file) throw new ErrorNotFound(); return respondWithFileContent(ctx.response, file); }); @@ -64,7 +60,7 @@ router.put('api/files/:id/content', async (path: SubPath, ctx: AppContext) => { // https://github.com/laurent22/joplin/issues/4402 const buffer = result?.files?.file ? await fs.readFile(result.files.file.path) : Buffer.alloc(0); - const file: File = await fileModel.entityFromItemId(fileId, { mustExist: false }); + const file: File = await fileModel.pathToFile(fileId, { mustExist: false, returnFullEntity: false }); file.content = buffer; return fileModel.toApiOutput(await fileModel.save(file, { validationRules: { mustBeFile: true } })); }); @@ -72,7 +68,7 @@ router.put('api/files/:id/content', async (path: SubPath, ctx: AppContext) => { router.del('api/files/:id/content', async (path: SubPath, ctx: AppContext) => { const fileModel = ctx.models.file({ userId: ctx.owner.id }); const fileId = path.id; - const file: File = await fileModel.entityFromItemId(fileId, { mustExist: false }); + const file: File = await fileModel.pathToFile(fileId, { mustExist: false, returnFullEntity: false }); if (!file) return; file.content = Buffer.alloc(0); await fileModel.save(file, { validationRules: { mustBeFile: true } }); @@ -80,22 +76,22 @@ router.del('api/files/:id/content', async (path: SubPath, ctx: AppContext) => { router.get('api/files/:id/delta', async (path: SubPath, ctx: AppContext) => { const fileModel = ctx.models.file({ userId: ctx.owner.id }); - const dir: File = await fileModel.entityFromItemId(path.id, { mustExist: true }); + const dirId: Uuid = await fileModel.pathToFileId(path.id); const changeModel = ctx.models.change({ userId: ctx.owner.id }); - return changeModel.byDirectoryId(dir.id, requestChangePagination(ctx.query)); + return changeModel.byDirectoryId(dirId, requestChangePagination(ctx.query)); }); router.get('api/files/:id/children', async (path: SubPath, ctx: AppContext) => { const fileModel = ctx.models.file({ userId: ctx.owner.id }); - const parent: File = await fileModel.entityFromItemId(path.id); - return fileModel.toApiOutput(await fileModel.childrens(parent.id, requestPagination(ctx.query))); + const parentId: Uuid = await fileModel.pathToFileId(path.id); + return fileModel.toApiOutput(await fileModel.childrens(parentId, requestPagination(ctx.query))); }); router.post('api/files/:id/children', async (path: SubPath, ctx: AppContext) => { const fileModel = ctx.models.file({ userId: ctx.owner.id }); const child: File = fileModel.fromApiInput(await bodyFields(ctx.req)); - const parent: File = await fileModel.entityFromItemId(path.id); - child.parent_id = parent.id; + const parentId: Uuid = await fileModel.pathToFileId(path.id); + child.parent_id = parentId; return fileModel.toApiOutput(await fileModel.save(child)); }); diff --git a/packages/server/src/routes/api/shares.test.ts b/packages/server/src/routes/api/shares.test.ts new file mode 100644 index 0000000000..f966e111e0 --- /dev/null +++ b/packages/server/src/routes/api/shares.test.ts @@ -0,0 +1,36 @@ +import { ShareType } from '../../db'; +import { putFileContent, testFilePath } from '../../utils/testing/fileApiUtils'; +import { getShareContext, postShareContext } from '../../utils/testing/shareApiUtils'; +import { beforeAllDb, afterAllTests, beforeEachDb, createUserAndSession } from '../../utils/testing/testUtils'; + +describe('api_shares', function() { + + beforeAll(async () => { + await beforeAllDb('api_shares'); + }); + + afterAll(async () => { + await afterAllTests(); + }); + + beforeEach(async () => { + await beforeEachDb(); + }); + + test('should share a file', async function() { + const { session } = await createUserAndSession(1, false); + const file = await putFileContent(session.id, 'root:/photo.jpg:', testFilePath()); + + const context = await postShareContext(session.id, 'root:/photo.jpg:'); + expect(context.response.status).toBe(200); + const shareId = context.response.body.id; + + { + const context = await getShareContext(shareId); + expect(context.response.body.id).toBe(shareId); + expect(context.response.body.file_id).toBe(file.id); + expect(context.response.body.type).toBe(ShareType.Link); + } + }); + +}); diff --git a/packages/server/src/routes/api/shares.ts b/packages/server/src/routes/api/shares.ts new file mode 100644 index 0000000000..7e3f4f8291 --- /dev/null +++ b/packages/server/src/routes/api/shares.ts @@ -0,0 +1,30 @@ +import { ErrorNotFound } from '../../utils/errors'; +import { Share } from '../../db'; +import { bodyFields, ownerRequired } from '../../utils/requestUtils'; +import { SubPath } from '../../utils/routeUtils'; +import Router from '../../utils/Router'; +import { AppContext } from '../../utils/types'; + +const router = new Router(); + +router.public = true; + +router.post('api/shares', async (_path: SubPath, ctx: AppContext) => { + ownerRequired(ctx); + + const shareModel = ctx.models.share({ userId: ctx.owner.id }); + const share: Share = shareModel.fromApiInput(await bodyFields(ctx.req)) as Share; + return shareModel.add(share.type, share.file_id); +}); + +router.get('api/shares/:id', async (path: SubPath, ctx: AppContext) => { + // No authentication is necessary - anyone who knows the share ID is allowed + // to access the file. It is essentially public. + + const shareModel = ctx.models.share(); + const share = await shareModel.load(path.id); + if (!share) throw new ErrorNotFound(); + return shareModel.toApiOutput(share); +}); + +export default router; diff --git a/packages/server/src/routes/default.ts b/packages/server/src/routes/default.ts index 73908dde0e..576c3ad4f3 100644 --- a/packages/server/src/routes/default.ts +++ b/packages/server/src/routes/default.ts @@ -1,10 +1,11 @@ -import * as Koa from 'koa'; import { SubPath, Response, ResponseType } from '../utils/routeUtils'; import Router from '../utils/Router'; import { ErrorNotFound, ErrorForbidden } from '../utils/errors'; import { dirname, normalize } from 'path'; import { pathExists } from 'fs-extra'; import * as fs from 'fs-extra'; +import { AppContext } from '../utils/types'; +import Applications from '../services/Applications'; const { mime } = require('@joplin/lib/mime-utils.js'); const publicDir = `${dirname(dirname(__dirname))}/public`; @@ -19,9 +20,15 @@ const pathToFileMap: PathToFileMap = { 'css/bulma.min.css': 'node_modules/bulma/css/bulma.min.css', 'css/bulma-prefers-dark.min.css': 'node_modules/bulma-prefers-dark/css/bulma-prefers-dark.min.css', 'css/fontawesome/css/all.min.css': 'node_modules/@fortawesome/fontawesome-free/css/all.min.css', + + // Hard-coded for now but it could be made dynamic later on + // 'apps/joplin/css/note.css': 'src/apps/joplin/css/note.css', }; -async function findLocalFile(path: string): Promise { +async function findLocalFile(path: string, apps: Applications): Promise { + const appFilePath = await apps.localFileFromUrl(path); + if (appFilePath) return appFilePath; + if (path in pathToFileMap) return pathToFileMap[path]; // For now a bit of a hack to load FontAwesome fonts. if (path.indexOf('css/fontawesome/webfonts/fa-') === 0) return `node_modules/@fortawesome/fontawesome-free/${path.substr(16)}`; @@ -43,8 +50,8 @@ router.public = true; // Used to serve static files, so it needs to be public because for example the // login page, which is public, needs access to the CSS files. -router.get('', async (path: SubPath, ctx: Koa.Context) => { - const localPath = await findLocalFile(path.raw); +router.get('', async (path: SubPath, ctx: AppContext) => { + const localPath = await findLocalFile(path.raw, ctx.apps); let mimeType: string = mime.fromFilename(localPath); if (!mimeType) mimeType = 'application/octet-stream'; diff --git a/packages/server/src/routes/index/files.ts b/packages/server/src/routes/index/files.ts index f13c5829ce..a7b9981f72 100644 --- a/packages/server/src/routes/index/files.ts +++ b/packages/server/src/routes/index/files.ts @@ -40,7 +40,7 @@ router.get('files/:id', async (path: SubPath, ctx: AppContext) => { const owner = ctx.owner; const fileModel = ctx.models.file({ userId: owner.id }); const root = await fileModel.userRootFile(); - const parentTemp: File = dirId ? await fileModel.entityFromItemId(dirId) : root; + const parentTemp: File = dirId ? await fileModel.pathToFile(dirId, { returnFullEntity: false }) : root; const parent: File = await fileModel.load(parentTemp.id); const paginatedFiles = await fileModel.childrens(parent.id, pagination); const pageCount = Math.ceil((await fileModel.childrenCount(parent.id)) / pagination.limit); @@ -97,7 +97,7 @@ router.get('files/:id', async (path: SubPath, ctx: AppContext) => { router.get('files/:id/content', async (path: SubPath, ctx: AppContext) => { const fileModel = ctx.models.file({ userId: ctx.owner.id }); - let file: File = await fileModel.entityFromItemId(path.id); + let file: File = await fileModel.pathToFile(path.id, { returnFullEntity: false }); file = await fileModel.loadWithContent(file.id); if (!file) throw new ErrorNotFound(); return respondWithFileContent(ctx.response, file); @@ -113,7 +113,7 @@ router.post('files', async (_path: SubPath, ctx: AppContext) => { if (fields.delete_all_button) { const fileModel = ctx.models.file({ userId: ctx.owner.id }); - const parent: File = await fileModel.entityFromItemId(parentId, { returnFullEntity: true }); + const parent: File = await fileModel.pathToFile(parentId, { returnFullEntity: false }); await fileModel.deleteChildren(parent.id); } else { throw new Error('Invalid form button'); diff --git a/packages/server/src/routes/index/shares.joplin.test.ts b/packages/server/src/routes/index/shares.joplin.test.ts new file mode 100644 index 0000000000..893e1b14cb --- /dev/null +++ b/packages/server/src/routes/index/shares.joplin.test.ts @@ -0,0 +1,203 @@ +import routeHandler from '../../middleware/routeHandler'; +import { putFileContent, testFilePath, postDirectory } from '../../utils/testing/fileApiUtils'; +import { postShare } from '../../utils/testing/shareApiUtils'; +import { beforeAllDb, afterAllTests, parseHtml, beforeEachDb, createUserAndSession, createFile, koaAppContext, checkContextError } from '../../utils/testing/testUtils'; + +const resourceSize = 2720; + +const resourceContents: Record = { + image: `Test Image + +id: 96765a68655f4446b3dbad7d41b6566e +mime: image/jpeg +filename: +created_time: 2020-10-15T10:37:58.090Z +updated_time: 2020-10-15T10:37:58.090Z +user_created_time: 2020-10-15T10:37:58.090Z +user_updated_time: 2020-10-15T10:37:58.090Z +file_extension: jpg +encryption_cipher_text: +encryption_applied: 0 +encryption_blob_encrypted: 0 +size: ${resourceSize} +is_shared: 0 +type_: 4`, + +}; + +const noteContents: Record = { + + simple: `Testing title + +Testing body + +id: b39dadd7a63742bebf3125fd2a9286d4 +parent_id: e98f305dde8b47b793f031cf883324ff +created_time: 2020-10-15T10:34:16.044Z +updated_time: 2021-01-28T23:10:30.054Z +is_conflict: 0 +latitude: 0.00000000 +longitude: 0.00000000 +altitude: 0.0000 +author: +source_url: +is_todo: 1 +todo_due: 1602760405000 +todo_completed: 0 +source: joplindev-desktop +source_application: net.cozic.joplindev-desktop +application_data: +order: 0 +user_created_time: 2020-10-15T10:34:16.044Z +user_updated_time: 2020-10-19T17:21:03.394Z +encryption_cipher_text: +encryption_applied: 0 +markup_language: 1 +is_shared: 1 +type_: 1`, + + katex: `Katex Test + +$\\sqrt{3x-1}+(1+x)^2$ + +id: b39dadd7a63742bebf3125fd2a9286d4 +parent_id: e98f305dde8b47b793f031cf883324ff +created_time: 2020-10-15T10:34:16.044Z +updated_time: 2021-01-28T23:10:30.054Z +is_conflict: 0 +latitude: 0.00000000 +longitude: 0.00000000 +altitude: 0.0000 +author: +source_url: +is_todo: 1 +todo_due: 1602760405000 +todo_completed: 0 +source: joplindev-desktop +source_application: net.cozic.joplindev-desktop +application_data: +order: 0 +user_created_time: 2020-10-15T10:34:16.044Z +user_updated_time: 2020-10-19T17:21:03.394Z +encryption_cipher_text: +encryption_applied: 0 +markup_language: 1 +is_shared: 1 +type_: 1`, + + image: `Image Test + +![my image](:/96765a68655f4446b3dbad7d41b6566e) + +id: b39dadd7a63742bebf3125fd2a9286d4 +parent_id: e98f305dde8b47b793f031cf883324ff +created_time: 2020-10-15T10:34:16.044Z +updated_time: 2021-01-28T23:10:30.054Z +is_conflict: 0 +latitude: 0.00000000 +longitude: 0.00000000 +altitude: 0.0000 +author: +source_url: +is_todo: 1 +todo_due: 1602760405000 +todo_completed: 0 +source: joplindev-desktop +source_application: net.cozic.joplindev-desktop +application_data: +order: 0 +user_created_time: 2020-10-15T10:34:16.044Z +user_updated_time: 2020-10-19T17:21:03.394Z +encryption_cipher_text: +encryption_applied: 0 +markup_language: 1 +is_shared: 1 +type_: 1`, + +}; + +async function getShareContent(shareId: string, query: any = {}): Promise { + const context = await koaAppContext({ + request: { + method: 'GET', + url: `/shares/${shareId}`, + query, + }, + }); + await routeHandler(context); + await checkContextError(context); + return context.response.body; +} + +describe('shares.joplin', function() { + + beforeAll(async () => { + await beforeAllDb('shares.joplin'); + }); + + afterAll(async () => { + await afterAllTests(); + }); + + beforeEach(async () => { + await beforeEachDb(); + }); + + test('should display a simple note', async function() { + const { user, session } = await createUserAndSession(); + + await createFile(user.id, 'root:/b39dadd7a63742bebf3125fd2a9286d4.md:', noteContents.simple); + + const share = await postShare(session.id, 'root:/b39dadd7a63742bebf3125fd2a9286d4.md:'); + + const bodyHtml = await getShareContent(share.id); + + // Check that a few important strings are present + expect(bodyHtml).toContain('rendered-md'); // Means we have the HTML body + expect(bodyHtml).toContain('Testing title'); // Means the note has been rendered + expect(bodyHtml).toContain('Testing body'); + }); + + test('should load plugins', async function() { + const { user, session } = await createUserAndSession(); + + await createFile(user.id, 'root:/b39dadd7a63742bebf3125fd2a9286d4.md:', noteContents.katex); + + const share = await postShare(session.id, 'root:/b39dadd7a63742bebf3125fd2a9286d4.md:'); + + const bodyHtml = await getShareContent(share.id); + + expect(bodyHtml).toContain('class="katex-mathml"'); + }); + + test('should render attached images', async function() { + const { user, session } = await createUserAndSession(); + + await createFile(user.id, 'root:/b39dadd7a63742bebf3125fd2a9286d4.md:', noteContents.image); + await postDirectory(session.id, 'root', '.resource'); + await putFileContent(session.id, 'root:/.resource/96765a68655f4446b3dbad7d41b6566e:', testFilePath()); + await createFile(user.id, 'root:/96765a68655f4446b3dbad7d41b6566e.md:', resourceContents.image); + + const share = await postShare(session.id, 'root:/b39dadd7a63742bebf3125fd2a9286d4.md:'); + + const bodyHtml = await getShareContent(share.id) as string; + + // We should get an image like this: + // + // + + const doc = parseHtml(bodyHtml); + const image = doc.querySelector('img[data-resource-id="96765a68655f4446b3dbad7d41b6566e"]'); + expect(image.getAttribute('src')).toBe(`http://localhost:22300/shares/${share.id}?resource_id=96765a68655f4446b3dbad7d41b6566e&t=1602758278090`); + + // If we try to get the resource, via the share link, we should get full + // image. + const resourceContent = await getShareContent(share.id, { + resource_id: '96765a68655f4446b3dbad7d41b6566e', + t: '1602758278090', + }) as Buffer; + + expect(resourceContent.byteLength).toBe(resourceSize); + }); + +}); diff --git a/packages/server/src/routes/index/shares.ts b/packages/server/src/routes/index/shares.ts new file mode 100644 index 0000000000..ef8da7242a --- /dev/null +++ b/packages/server/src/routes/index/shares.ts @@ -0,0 +1,45 @@ +import { SubPath, ResponseType, Response } from '../../utils/routeUtils'; +import Router from '../../utils/Router'; +import { AppContext } from '../../utils/types'; +import { ErrorNotFound } from '../../utils/errors'; +import { File, Share } from '../../db'; +import { FileViewerResponse } from '../../apps/joplin/Application'; + +async function renderFile(context: AppContext, file: File, share: Share): Promise { + const joplinApp = await context.apps.joplin(); + + if (await joplinApp.isItemFile(file)) { + return joplinApp.renderFile(file, share, context.query); + } + + return { + body: file.content, + mime: file.mime_type, + size: file.size, + }; +} + +const router: Router = new Router(); + +router.public = true; + +router.get('shares/:id', async (path: SubPath, ctx: AppContext) => { + const fileModel = ctx.models.file(); + const shareModel = ctx.models.share(); + + const share = await shareModel.load(path.id); + if (!share) throw new ErrorNotFound(); + + const file = await fileModel.loadWithContent(share.file_id, { skipPermissionCheck: true }); + if (!file) throw new ErrorNotFound(); + + + const result = await renderFile(ctx, file, share); + + ctx.response.body = result.body; + ctx.response.set('Content-Type', result.mime); + ctx.response.set('Content-Length', result.size.toString()); + return new Response(ResponseType.KoaResponse, ctx.response); +}); + +export default router; diff --git a/packages/server/src/routes/index/users.test.ts b/packages/server/src/routes/index/users.test.ts index 5b5928e2c1..b94c974aee 100644 --- a/packages/server/src/routes/index/users.test.ts +++ b/packages/server/src/routes/index/users.test.ts @@ -1,7 +1,6 @@ import { File, User } from '../../db'; import routeHandler from '../../middleware/routeHandler'; -import { checkContextError } from '../../utils/testing/apiUtils'; -import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, createUserAndSession, models, parseHtml } from '../../utils/testing/testUtils'; +import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, createUserAndSession, models, parseHtml, checkContextError } from '../../utils/testing/testUtils'; export async function postUser(sessionId: string, email: string, password: string): Promise { const context = await koaAppContext({ diff --git a/packages/server/src/routes/routes.ts b/packages/server/src/routes/routes.ts index ad3b803893..7f068f1f6c 100644 --- a/packages/server/src/routes/routes.ts +++ b/packages/server/src/routes/routes.ts @@ -3,25 +3,31 @@ import { Routers } from '../utils/routeUtils'; import apiSessions from './api/sessions'; import apiPing from './api/ping'; import apiFiles from './api/files'; -import indexLoginRoute from './index/login'; -import indexLogoutRoute from './index/logout'; -import indexHomeRoute from './index/home'; -import indexUsersRoute from './index/users'; -import indexFilesRoute from './index/files'; -import indexNotificationsRoute from './index/notifications'; +import apiShares from './api/shares'; + +import indexLogin from './index/login'; +import indexLogout from './index/logout'; +import indexHome from './index/home'; +import indexUsers from './index/users'; +import indexFiles from './index/files'; +import indexNotifications from './index/notifications'; +import indexShares from './index/shares'; + import defaultRoute from './default'; const routes: Routers = { 'api/ping': apiPing, 'api/sessions': apiSessions, 'api/files': apiFiles, + 'api/shares': apiShares, - 'login': indexLoginRoute, - 'logout': indexLogoutRoute, - 'home': indexHomeRoute, - 'users': indexUsersRoute, - 'files': indexFilesRoute, - 'notifications': indexNotificationsRoute, + 'login': indexLogin, + 'logout': indexLogout, + 'home': indexHome, + 'users': indexUsers, + 'files': indexFiles, + 'notifications': indexNotifications, + 'shares': indexShares, '': defaultRoute, }; diff --git a/packages/server/src/services/Applications.ts b/packages/server/src/services/Applications.ts new file mode 100644 index 0000000000..d70aeda9f3 --- /dev/null +++ b/packages/server/src/services/Applications.ts @@ -0,0 +1,48 @@ +import ApplicationJoplin from '../apps/joplin/Application'; +import config from '../config'; +import { Models } from '../models/factory'; + +export default class Applications { + + private joplin_: ApplicationJoplin = null; + private models_: Models; + + public constructor(models: Models) { + this.models_ = models; + } + + public async joplin(): Promise { + if (!this.joplin_) { + this.joplin_ = new ApplicationJoplin(); + this.joplin_.initBase_('joplin', config(), this.models_); + await this.joplin_.initialize(); + } + + return this.joplin_; + } + + public async localFileFromUrl(url: string): Promise { + if (url.indexOf('apps/') !== 0) return null; + + // The below is roughtly hard-coded for the Joplin app but could be + // generalised if multiple apps are supported. + + const joplinApp = await this.joplin(); + + const fromAppUrl = await joplinApp.localFileFromUrl(url); + if (fromAppUrl) return fromAppUrl; + + const rootDir = joplinApp.rootDir; + + const defaultPaths = [ + 'apps/joplin', + ]; + + for (const p of defaultPaths) { + if (url.indexOf(p) === 0) return `${rootDir}/${url.substr(p.length + 1)}`; + } + + return null; + } + +} diff --git a/packages/server/src/services/BaseApplication.ts b/packages/server/src/services/BaseApplication.ts new file mode 100644 index 0000000000..73a9b49d5b --- /dev/null +++ b/packages/server/src/services/BaseApplication.ts @@ -0,0 +1,45 @@ +import { Models } from '../models/factory'; +import { Config } from '../utils/types'; +import MustacheService from './MustacheService'; + +export default class BaseApplication { + + private appName_: string; + private config_: Config = null; + private models_: Models = null; + private mustache_: MustacheService = null; + private rootDir_: string; + + protected get mustache(): MustacheService { + return this.mustache_; + } + + protected get config(): Config { + return this.config_; + } + + protected get models(): Models { + return this.models_; + } + + public get rootDir(): string { + return this.rootDir_; + } + + public get appBaseUrl(): string { + return `${this.config.baseUrl}/apps/${this.appName_}`; + } + + public initBase_(appName: string, config: Config, models: Models) { + this.appName_ = appName; + this.rootDir_ = `${config.rootDir}/src/apps/${appName}`; + this.config_ = config; + this.models_ = models; + this.mustache_ = new MustacheService(`${this.rootDir}/views`, `${config.baseUrl}/apps/${appName}`); + } + + public async localFileFromUrl(_url: string): Promise { + return null; + } + +} diff --git a/packages/server/src/services/MustacheService.ts b/packages/server/src/services/MustacheService.ts index 14e50f3619..630eff615c 100644 --- a/packages/server/src/services/MustacheService.ts +++ b/packages/server/src/services/MustacheService.ts @@ -22,7 +22,24 @@ export function isView(o: any): boolean { return 'path' in o && 'name' in o; } -class MustacheService { +export default class MustacheService { + + private viewDir_: string; + private baseAssetUrl_: string; + private prefersDarkEnabled_: boolean = true; + + public constructor(viewDir: string, baseAssetUrl: string) { + this.viewDir_ = viewDir; + this.baseAssetUrl_ = baseAssetUrl; + } + + public get prefersDarkEnabled(): boolean { + return this.prefersDarkEnabled_; + } + + public set prefersDarkEnabled(v: boolean) { + this.prefersDarkEnabled_ = v; + } private get defaultLayoutPath(): string { return `${config().layoutDir}/default.mustache`; @@ -31,6 +48,7 @@ class MustacheService { private get defaultLayoutOptions(): any { return { baseUrl: config().baseUrl, + prefersDarkEnabled: this.prefersDarkEnabled_, }; } @@ -41,7 +59,7 @@ class MustacheService { private resolvesFilePaths(type: string, paths: string[]): string[] { const output: string[] = []; for (const path of paths) { - output.push(`${config().baseUrl}/${type}/${path}.${type}`); + output.push(`${this.baseAssetUrl_}/${type}/${path}.${type}`); } return output; } @@ -53,11 +71,11 @@ class MustacheService { const partialContents: any = {}; for (const partialName of partials) { - const filePath = `${config().viewDir}/partials/${partialName}.mustache`; + const filePath = `${this.viewDir_}/partials/${partialName}.mustache`; partialContents[partialName] = await this.loadTemplateContent(filePath); } - const filePath = `${config().viewDir}/${view.path}.mustache`; + const filePath = `${this.viewDir_}/${view.path}.mustache`; globalParams = { ...this.defaultLayoutOptions, @@ -86,7 +104,3 @@ class MustacheService { } } - -const mustacheService = new MustacheService(); - -export default mustacheService; diff --git a/packages/server/src/tools/db-migrate.ts b/packages/server/src/tools/db-migrate.ts deleted file mode 100644 index d69c8690bf..0000000000 --- a/packages/server/src/tools/db-migrate.ts +++ /dev/null @@ -1,27 +0,0 @@ -// import db, { dbConfig } from '../app/db'; - -// // require('source-map-support').install(); - -// const config = { -// directory: `${__dirname}/../migrations`, -// // Disable transactions because the models might open one too -// disableTransactions: true, -// }; - -// console.info(`Using database: ${dbConfig().connection.filename}`); -// console.info(`Running migrations in: ${config.directory}`); - -// db().migrate.latest(config).then((event: any) => { -// const log: string[] = event[1]; - -// if (!log.length) { -// console.info('Database is already up to date'); -// } else { -// console.info(`Ran migrations: ${log.join(', ')}`); -// } - -// db().destroy(); -// }).catch((error:any) => { -// console.error(error); -// process.exit(1); -// }); diff --git a/packages/server/src/tools/generate-types.ts b/packages/server/src/tools/generateTypes.ts similarity index 89% rename from packages/server/src/tools/generate-types.ts rename to packages/server/src/tools/generateTypes.ts index a1ff1953a9..29f3d52a05 100644 --- a/packages/server/src/tools/generate-types.ts +++ b/packages/server/src/tools/generateTypes.ts @@ -29,9 +29,18 @@ const config = { 'main.api_clients': 'WithDates, WithUuid', 'main.changes': 'WithDates, WithUuid', 'main.notifications': 'WithDates, WithUuid', + 'main.shares': 'WithDates, WithUuid', }, }; +const propertyTypes: Record = { + '*.item_type': 'ItemType', + 'files.content': 'Buffer', + 'changes.type': 'ChangeType', + 'notifications.level': 'NotificationLevel', + 'shares.type': 'ShareType', +}; + function insertContentIntoFile(filePath: string, markerOpen: string, markerClose: string, contentToInsert: string): void { const fs = require('fs'); if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`); @@ -64,10 +73,8 @@ function createTypeString(table: any) { if (['id'].includes(name)) continue; } - if (name === 'item_type') type = 'ItemType'; - if (table.name === 'files' && name === 'content') type = 'Buffer'; - if (table.name === 'changes' && name === 'type') type = 'ChangeType'; - if (table.name === 'notifications' && name === 'level') type = 'NotificationLevel'; + if (propertyTypes[`*.${name}`]) type = propertyTypes[`*.${name}`]; + if (propertyTypes[`${table.name}.${name}`]) type = propertyTypes[`${table.name}.${name}`]; if ((name === 'id' || name.endsWith('_id') || name === 'uuid') && type === 'string') type = 'Uuid'; colStrings.push(`\t${name}?: ${type};`); diff --git a/packages/server/src/utils/requestUtils.ts b/packages/server/src/utils/requestUtils.ts index 093a149425..ae61781b66 100644 --- a/packages/server/src/utils/requestUtils.ts +++ b/packages/server/src/utils/requestUtils.ts @@ -50,6 +50,10 @@ export async function bodyFields(req: any): Promise { return form.fields; } +export function ownerRequired(ctx: AppContext) { + if (!ctx.owner) throw new ErrorForbidden(); +} + export function headerSessionId(headers: any): string { return headers['x-api-auth'] ? headers['x-api-auth'] : ''; } diff --git a/packages/server/src/utils/setupAppContext.ts b/packages/server/src/utils/setupAppContext.ts new file mode 100644 index 0000000000..3da28b859f --- /dev/null +++ b/packages/server/src/utils/setupAppContext.ts @@ -0,0 +1,14 @@ +import { LoggerWrapper } from '@joplin/lib/Logger'; +import config from '../config'; +import { DbConnection } from '../db'; +import newModelFactory from '../models/factory'; +import Applications from '../services/Applications'; +import { AppContext, Env } from './types'; + +export default async function(appContext: AppContext, env: Env, dbConnection: DbConnection, appLogger: ()=> LoggerWrapper) { + appContext.env = env; + appContext.db = dbConnection; + appContext.models = newModelFactory(appContext.db, config().baseUrl); + appContext.apps = new Applications(appContext.models); + appContext.appLogger = appLogger; +} diff --git a/packages/server/src/utils/testing/apiUtils.ts b/packages/server/src/utils/testing/fileApiUtils.ts similarity index 96% rename from packages/server/src/utils/testing/apiUtils.ts rename to packages/server/src/utils/testing/fileApiUtils.ts index 0130401c3c..b1cbbdd8d8 100644 --- a/packages/server/src/utils/testing/apiUtils.ts +++ b/packages/server/src/utils/testing/fileApiUtils.ts @@ -12,10 +12,11 @@ import { File } from '../../db'; import routeHandler from '../../middleware/routeHandler'; import { PaginatedResults, Pagination, paginationToQueryParams } from '../../models/utils/pagination'; import { AppContext } from '../types'; -import { koaAppContext } from './testUtils'; +import { checkContextError, koaAppContext, testAssetDir } from './testUtils'; -export function checkContextError(context: AppContext) { - if (context.response.status >= 400) throw new Error(JSON.stringify(context.response)); +export function testFilePath(ext: string = 'jpg') { + const basename = ext === 'jpg' ? 'photo' : 'poster'; + return `${testAssetDir}/${basename}.${ext}`; } export async function getFileMetadataContext(sessionId: string, path: string): Promise { diff --git a/packages/server/src/utils/testing/shareApiUtils.ts b/packages/server/src/utils/testing/shareApiUtils.ts new file mode 100644 index 0000000000..1f9b2e058f --- /dev/null +++ b/packages/server/src/utils/testing/shareApiUtils.ts @@ -0,0 +1,43 @@ +import { Share, ShareType, Uuid } from '../../db'; +import routeHandler from '../../middleware/routeHandler'; +import { AppContext } from '../types'; +import { checkContextError, koaAppContext } from './testUtils'; + +export async function postShareContext(sessionId: string, itemId: Uuid): Promise { + const context = await koaAppContext({ + sessionId: sessionId, + request: { + method: 'POST', + url: '/api/shares', + body: { + file_id: itemId, + type: ShareType.Link, + }, + }, + }); + await routeHandler(context); + return context; +} + +export async function postShare(sessionId: string, itemId: Uuid): Promise { + const context = await postShareContext(sessionId, itemId); + checkContextError(context); + return context.response.body; +} + +export async function getShareContext(shareId: Uuid): Promise { + const context = await koaAppContext({ + request: { + method: 'GET', + url: `/api/shares/${shareId}`, + }, + }); + await routeHandler(context); + return context; +} + +export async function getShare(shareId: Uuid): Promise { + const context = await getShareContext(shareId); + checkContextError(context); + return context.response.body; +} diff --git a/packages/server/src/utils/testing/testUtils.ts b/packages/server/src/utils/testing/testUtils.ts index 43d038f6f9..bd864e040f 100644 --- a/packages/server/src/utils/testing/testUtils.ts +++ b/packages/server/src/utils/testing/testUtils.ts @@ -12,6 +12,7 @@ import * as httpMocks from 'node-mocks-http'; import * as crypto from 'crypto'; import * as fs from 'fs-extra'; import * as jsdom from 'jsdom'; +import setupAppContext from '../setupAppContext'; // Takes into account the fact that this file will be inside the /dist directory // when it runs. @@ -111,6 +112,8 @@ export async function koaAppContext(options: AppContextTestOptions = null): Prom // don't need to mock all of them. const appContext: any = {}; + await setupAppContext(appContext, Env.Dev, db_, () => appLogger); + appContext.env = Env.Dev; appContext.db = db_; appContext.models = models(); @@ -199,6 +202,21 @@ export async function createFileTree(fileModel: FileModel, parentId: string, tre } } +export async function createFile(userId: string, path: string, content: string): Promise { + const fileModel = models().file({ userId }); + const file: File = await fileModel.pathToFile(path, { mustExist: false, returnFullEntity: false }); + file.content = Buffer.from(content); + const savedFile = await fileModel.save(file); + return fileModel.load(savedFile.id); +} + +export function checkContextError(context: AppContext) { + if (context.response.status >= 400) { + // console.info(context.response.body); + throw new Error(`${context.method} ${context.path} ${JSON.stringify(context.response)}`); + } +} + export async function checkThrowAsync(asyncFn: Function): Promise { try { await asyncFn(); diff --git a/packages/server/src/utils/types.ts b/packages/server/src/utils/types.ts index 271eb018d6..0f922fa2e7 100644 --- a/packages/server/src/utils/types.ts +++ b/packages/server/src/utils/types.ts @@ -2,6 +2,7 @@ import { LoggerWrapper } from '@joplin/lib/Logger'; import * as Koa from 'koa'; import { DbConnection, User, Uuid } from '../db'; import { Models } from '../models/factory'; +import Applications from '../services/Applications'; export enum Env { Dev = 'dev', @@ -23,6 +24,7 @@ export interface AppContext extends Koa.Context { appLogger(): LoggerWrapper; notifications: NotificationView[]; owner: User; + apps: Applications; } export enum DatabaseConfigClient { @@ -48,6 +50,7 @@ export interface Config { // Not that, for now, nothing is being logged to file. Log is just printed // to stdout, which is then handled by Docker own log mechanism logDir: string; + tempDir: string; database: DatabaseConfig; baseUrl: string; } diff --git a/packages/server/src/views/layouts/default.mustache b/packages/server/src/views/layouts/default.mustache index a7f8d489b3..1ef56703d1 100644 --- a/packages/server/src/views/layouts/default.mustache +++ b/packages/server/src/views/layouts/default.mustache @@ -3,13 +3,18 @@ - + {{#global.prefersDarkEnabled}} + + {{/global.prefersDarkEnabled}} {{#cssFiles}} {{/cssFiles}} + {{#jsFiles}} + + {{/jsFiles}} {{> navbar}} diff --git a/packages/tools/generate-database-types.js b/packages/tools/generate-database-types.js index 85d199a507..92ef3737cc 100644 --- a/packages/tools/generate-database-types.js +++ b/packages/tools/generate-database-types.js @@ -1,41 +1,61 @@ -const { execCommandVerbose, rootDir } = require('./tool-utils'); +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const tool_utils_1 = require("./tool-utils"); const sqlts = require('@rmp135/sql-ts').default; const fs = require('fs-extra'); - -async function main() { - // Run the CLI app once so as to generate the database file - process.chdir(`${rootDir}/packages/app-cli`); - await execCommandVerbose('npm', ['start', '--', 'version']); - - const sqlTsConfig = { - 'client': 'sqlite3', - 'connection': { - 'filename': `${require('os').homedir()}/.config/joplindev-desktop/database.sqlite`, - }, - 'tableNameCasing': 'pascal', - 'singularTableNames': true, - 'useNullAsDefault': true, // To disable warning "sqlite does not support inserting default values" - 'excludedTables': [ - 'main.notes_fts', - 'main.notes_fts_segments', - 'main.notes_fts_segdir', - 'main.notes_fts_docsize', - 'main.notes_fts_stat', - ], - }; - - const definitions = await sqlts.toObject(sqlTsConfig); - - const tsString = sqlts.fromObject(definitions, sqlTsConfig) - .replace(/": /g, '"?: '); - const header = `// AUTO-GENERATED BY ${__filename.substr(rootDir.length + 1)}`; - - const targetFile = `${rootDir}/packages/lib/services/database/types.ts`; - console.info(`Writing type definitions to ${targetFile}...`); - await fs.writeFile(targetFile, `${header}\n\n${tsString}`, 'utf8'); +function main() { + return __awaiter(this, void 0, void 0, function* () { + // Run the CLI app once so as to generate the database file + process.chdir(`${tool_utils_1.rootDir}/packages/app-cli`); + yield tool_utils_1.execCommand2('npm start -- version'); + const sqlTsConfig = { + 'client': 'sqlite3', + 'connection': { + 'filename': `${require('os').homedir()}/.config/joplindev-desktop/database.sqlite`, + }, + 'tableNameCasing': 'pascal', + 'singularTableNames': true, + 'useNullAsDefault': true, + 'excludedTables': [ + 'main.notes_fts', + 'main.notes_fts_segments', + 'main.notes_fts_segdir', + 'main.notes_fts_docsize', + 'main.notes_fts_stat', + ], + }; + const definitions = yield sqlts.toObject(sqlTsConfig); + definitions.tables = definitions.tables.map((t) => { + t.columns.push({ + nullable: false, + name: 'type_', + type: 'int', + optional: true, + isEnum: false, + propertyName: 'type_', + propertyType: 'number', + }); + return t; + }); + const tsString = sqlts.fromObject(definitions, sqlTsConfig) + .replace(/": /g, '"?: '); + const header = `// AUTO-GENERATED BY ${__filename.substr(tool_utils_1.rootDir.length + 1)}`; + const targetFile = `${tool_utils_1.rootDir}/packages/lib/services/database/types.ts`; + console.info(`Writing type definitions to ${targetFile}...`); + yield fs.writeFile(targetFile, `${header}\n\n${tsString}`, 'utf8'); + }); } - main().catch((error) => { - console.error(error); - process.exit(1); + console.error(error); + process.exit(1); }); +//# sourceMappingURL=generate-database-types.js.map \ No newline at end of file diff --git a/packages/tools/generate-database-types.ts b/packages/tools/generate-database-types.ts new file mode 100644 index 0000000000..4056684c3e --- /dev/null +++ b/packages/tools/generate-database-types.ts @@ -0,0 +1,56 @@ +import { execCommand2, rootDir } from './tool-utils'; + +const sqlts = require('@rmp135/sql-ts').default; +const fs = require('fs-extra'); + +async function main() { + // Run the CLI app once so as to generate the database file + process.chdir(`${rootDir}/packages/app-cli`); + await execCommand2('npm start -- version'); + + const sqlTsConfig = { + 'client': 'sqlite3', + 'connection': { + 'filename': `${require('os').homedir()}/.config/joplindev-desktop/database.sqlite`, + }, + 'tableNameCasing': 'pascal', + 'singularTableNames': true, + 'useNullAsDefault': true, // To disable warning "sqlite does not support inserting default values" + 'excludedTables': [ + 'main.notes_fts', + 'main.notes_fts_segments', + 'main.notes_fts_segdir', + 'main.notes_fts_docsize', + 'main.notes_fts_stat', + ], + }; + + const definitions = await sqlts.toObject(sqlTsConfig); + + definitions.tables = definitions.tables.map((t: any) => { + t.columns.push({ + nullable: false, + name: 'type_', + type: 'int', + optional: true, + isEnum: false, + propertyName: 'type_', + propertyType: 'number', + }); + + return t; + }); + + const tsString = sqlts.fromObject(definitions, sqlTsConfig) + .replace(/": /g, '"?: '); + const header = `// AUTO-GENERATED BY ${__filename.substr(rootDir.length + 1)}`; + + const targetFile = `${rootDir}/packages/lib/services/database/types.ts`; + console.info(`Writing type definitions to ${targetFile}...`); + await fs.writeFile(targetFile, `${header}\n\n${tsString}`, 'utf8'); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/packages/tools/gulp/tasks/updateIgnoredTypeScriptBuild.js b/packages/tools/gulp/tasks/updateIgnoredTypeScriptBuild.js index c1267ffb71..37ba023fc1 100644 --- a/packages/tools/gulp/tasks/updateIgnoredTypeScriptBuild.js +++ b/packages/tools/gulp/tasks/updateIgnoredTypeScriptBuild.js @@ -21,6 +21,7 @@ module.exports = { 'packages/app-mobile/android/**/*', 'packages/app-mobile/ios/**/*', 'packages/lib/plugin_types/**/*', + 'packages/server/**/*', ], }).filter(f => !f.endsWith('.d.ts')); diff --git a/readme/spec/server_delta_sharing.md b/readme/spec/server_delta_sharing.md new file mode 100644 index 0000000000..3b20e1a542 --- /dev/null +++ b/readme/spec/server_delta_sharing.md @@ -0,0 +1,35 @@ +# Joplin Server sharing feature + +## Sharing a file via a public URL + +Joplin Server is essentially a file hosting service and it allows sharing files via public URLs. To do so, an API call is made to `/api/shares` with the ID or path of the file that needs to be shared. This call returns a SHAREID that is then used to access the file via URL. When viewing the file, it will display it according to its mime type. Thus by default a Markdown file will be displayed as plain text. + +## Sharing a note via a public URL + +It is built on top of the file sharing feature. The file corresponding to the note is shared via the above API. Then a separate application, specific to Joplin, read and parse the Markdown file, and display it as note. + +That application works as a viewer - instead of displaying the Markdown file as plain text (by default), it renders it and displays it as HTML. + +The rendering engine is the same as the main applications, which allows us to use the same plugins and settings. + +### Attached resources + +Any resource attached to the note is also shared - so for example images will be displayed, and it will be possible to open any attached PDF. This + +### Linked note + +Any linked note will **not** be shared, due to the following reasons: + +- Privacy issue - you don't want to accidentally share a note just because it was linked to another note. + +- Even if the linked note has been shared separately, we still don't give access to it. We don't know who that link has been shared with - it could be a different recipient. + +### Multiple share links for a given note + +It should be possible to have multiple share links for a given note. For example: I share a note with one person, then the same note with a different person. I revoke the share for one person, but I sill want the other person to access the note. + +So when a share link is created for a note, the API always return a new link. + +## Sharing a note with a user + +TBD \ No newline at end of file