@rapidrest/service-core 1.0.0-rc.9 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +382 -23
- package/README.md +79 -5
- package/dist/lib/ApiErrors.js +10 -0
- package/dist/lib/ApiErrors.js.map +1 -1
- package/dist/lib/BackgroundService.js +1 -4
- package/dist/lib/BackgroundService.js.map +1 -1
- package/dist/lib/BackgroundServiceManager.js +47 -6
- package/dist/lib/BackgroundServiceManager.js.map +1 -1
- package/dist/lib/BulkError.js +4 -0
- package/dist/lib/BulkError.js.map +1 -1
- package/dist/lib/EventListenerManager.js +53 -18
- package/dist/lib/EventListenerManager.js.map +1 -1
- package/dist/lib/NetUtils.js +1 -0
- package/dist/lib/NetUtils.js.map +1 -1
- package/dist/lib/NotificationUtils.js +27 -8
- package/dist/lib/NotificationUtils.js.map +1 -1
- package/dist/lib/ObjectFactory.js +25 -44
- package/dist/lib/ObjectFactory.js.map +1 -1
- package/dist/lib/OpenApiSpec.js +31 -11
- package/dist/lib/OpenApiSpec.js.map +1 -1
- package/dist/lib/Server.js +74 -110
- package/dist/lib/Server.js.map +1 -1
- package/dist/lib/auth/AuthMiddleware.js +11 -16
- package/dist/lib/auth/AuthMiddleware.js.map +1 -1
- package/dist/lib/auth/JWTStrategy.js +68 -17
- package/dist/lib/auth/JWTStrategy.js.map +1 -1
- package/dist/lib/auth/index.js +0 -1
- package/dist/lib/auth/index.js.map +1 -1
- package/dist/lib/database/ConnectionKinds.js +15 -0
- package/dist/lib/database/ConnectionKinds.js.map +1 -1
- package/dist/lib/database/ConnectionManager.js +140 -61
- package/dist/lib/database/ConnectionManager.js.map +1 -1
- package/dist/lib/database/MongoConnection.js +13 -3
- package/dist/lib/database/MongoConnection.js.map +1 -1
- package/dist/lib/database/MongoRepository.js +61 -15
- package/dist/lib/database/MongoRepository.js.map +1 -1
- package/dist/lib/database/MongoSchemaSync.js +3 -3
- package/dist/lib/database/MongoSchemaSync.js.map +1 -1
- package/dist/lib/database/NamingUtils.js +3 -2
- package/dist/lib/database/NamingUtils.js.map +1 -1
- package/dist/lib/database/RedisCache.js +64 -0
- package/dist/lib/database/RedisCache.js.map +1 -0
- package/dist/lib/database/TypeOrmSupport.js +53 -13
- package/dist/lib/database/TypeOrmSupport.js.map +1 -1
- package/dist/lib/database/index.js +1 -0
- package/dist/lib/database/index.js.map +1 -1
- package/dist/lib/decorators/DatabaseDecorators.js +264 -20
- package/dist/lib/decorators/DatabaseDecorators.js.map +1 -1
- package/dist/lib/decorators/DocDecorators.js +30 -9
- package/dist/lib/decorators/DocDecorators.js.map +1 -1
- package/dist/lib/decorators/EventDecorators.js +1 -0
- package/dist/lib/decorators/EventDecorators.js.map +1 -1
- package/dist/lib/decorators/ModelDecorators.js +39 -23
- package/dist/lib/decorators/ModelDecorators.js.map +1 -1
- package/dist/lib/decorators/PersistenceDecorators.js +4 -3
- package/dist/lib/decorators/PersistenceDecorators.js.map +1 -1
- package/dist/lib/decorators/RouteDecorators.js +139 -36
- package/dist/lib/decorators/RouteDecorators.js.map +1 -1
- package/dist/lib/http/IWebSocketShim.js +2 -0
- package/dist/lib/http/IWebSocketShim.js.map +1 -0
- package/dist/lib/http/MiddlewareChain.js +167 -0
- package/dist/lib/http/MiddlewareChain.js.map +1 -0
- package/dist/lib/http/RuntimeDetect.js +9 -0
- package/dist/lib/http/RuntimeDetect.js.map +1 -0
- package/dist/lib/http/bun/BunAdapters.js +326 -0
- package/dist/lib/http/bun/BunAdapters.js.map +1 -0
- package/dist/lib/http/bun/BunRouter.js +391 -0
- package/dist/lib/http/bun/BunRouter.js.map +1 -0
- package/dist/lib/http/bun/BunWebSocket.js +49 -0
- package/dist/lib/http/bun/BunWebSocket.js.map +1 -0
- package/dist/lib/http/index.js +16 -3
- package/dist/lib/http/index.js.map +1 -1
- package/dist/lib/http/session/RedisSessionStore.js +30 -0
- package/dist/lib/http/session/RedisSessionStore.js.map +1 -0
- package/dist/lib/http/session/SessionManager.js +113 -0
- package/dist/lib/http/session/SessionManager.js.map +1 -0
- package/dist/lib/http/session/SessionStore.js +6 -0
- package/dist/lib/http/session/SessionStore.js.map +1 -0
- package/dist/lib/http/session/sessionMiddleware.js +42 -0
- package/dist/lib/http/session/sessionMiddleware.js.map +1 -0
- package/dist/lib/http/types.js +0 -3
- package/dist/lib/http/types.js.map +1 -1
- package/dist/lib/http/uWS/Adapters.js +421 -0
- package/dist/lib/http/uWS/Adapters.js.map +1 -0
- package/dist/lib/http/{Router.js → uWS/Router.js} +73 -173
- package/dist/lib/http/uWS/Router.js.map +1 -0
- package/dist/lib/http/{WebSocket.js → uWS/WebSocket.js} +4 -1
- package/dist/lib/http/uWS/WebSocket.js.map +1 -0
- package/dist/lib/models/BaseEntity.js +2 -1
- package/dist/lib/models/BaseEntity.js.map +1 -1
- package/dist/lib/models/BaseMongoEntity.js +2 -2
- package/dist/lib/models/BaseMongoEntity.js.map +1 -1
- package/dist/lib/models/ModelUtils.js +248 -101
- package/dist/lib/models/ModelUtils.js.map +1 -1
- package/dist/lib/models/RecoverableBaseEntity.js +6 -1
- package/dist/lib/models/RecoverableBaseEntity.js.map +1 -1
- package/dist/lib/models/RecoverableBaseMongoEntity.js +1 -0
- package/dist/lib/models/RecoverableBaseMongoEntity.js.map +1 -1
- package/dist/lib/models/RepoUtils.js +641 -213
- package/dist/lib/models/RepoUtils.js.map +1 -1
- package/dist/lib/models/SimpleEntity.js +1 -0
- package/dist/lib/models/SimpleEntity.js.map +1 -1
- package/dist/lib/models/SimpleMongoEntity.js +2 -1
- package/dist/lib/models/SimpleMongoEntity.js.map +1 -1
- package/dist/lib/models/StatusExtraData.js +4 -0
- package/dist/lib/models/StatusExtraData.js.map +1 -1
- package/dist/lib/routes/{AdminRoute.js → BaseAdminRoute.js} +98 -80
- package/dist/lib/routes/BaseAdminRoute.js.map +1 -0
- package/dist/lib/routes/{MetricsRoute.js → BaseMetricsRoute.js} +29 -15
- package/dist/lib/routes/BaseMetricsRoute.js.map +1 -0
- package/dist/lib/routes/{OpenAPIRoute.js → BaseOpenAPIRoute.js} +32 -32
- package/dist/lib/routes/BaseOpenAPIRoute.js.map +1 -0
- package/dist/lib/routes/BasePushRoute.js +305 -0
- package/dist/lib/routes/BasePushRoute.js.map +1 -0
- package/dist/lib/routes/BaseStaticRoute.js +153 -0
- package/dist/lib/routes/BaseStaticRoute.js.map +1 -0
- package/dist/lib/routes/BaseStatusRoute.js +80 -0
- package/dist/lib/routes/BaseStatusRoute.js.map +1 -0
- package/dist/lib/routes/CRUDRoute.js +284 -0
- package/dist/lib/routes/CRUDRoute.js.map +1 -0
- package/dist/lib/routes/ModelRoute.js +84 -57
- package/dist/lib/routes/ModelRoute.js.map +1 -1
- package/dist/lib/routes/RouteUtils.js +108 -20
- package/dist/lib/routes/RouteUtils.js.map +1 -1
- package/dist/lib/routes/index.js +7 -4
- package/dist/lib/routes/index.js.map +1 -1
- package/dist/lib/security/ACLUtils.js +297 -170
- package/dist/lib/security/ACLUtils.js.map +1 -1
- package/dist/lib/security/AccessControlList.js +18 -10
- package/dist/lib/security/AccessControlList.js.map +1 -1
- package/dist/lib/security/AccessControlListMongo.js +19 -68
- package/dist/lib/security/AccessControlListMongo.js.map +1 -1
- package/dist/lib/security/AccessControlListSQL.js +21 -70
- package/dist/lib/security/AccessControlListSQL.js.map +1 -1
- package/dist/lib/security/BaseACLRoute.js +239 -0
- package/dist/lib/security/BaseACLRoute.js.map +1 -0
- package/dist/lib/security/index.js +3 -0
- package/dist/lib/security/index.js.map +1 -1
- package/dist/lib/test/request.js +62 -5
- package/dist/lib/test/request.js.map +1 -1
- package/dist/lib/test/requestws.js +34 -10
- package/dist/lib/test/requestws.js.map +1 -1
- package/dist/types/ApiErrors.d.ts +8 -2
- package/dist/types/BackgroundService.d.ts +0 -5
- package/dist/types/BackgroundServiceManager.d.ts +2 -1
- package/dist/types/EventListenerManager.d.ts +7 -6
- package/dist/types/NotificationUtils.d.ts +5 -3
- package/dist/types/OpenApiSpec.d.ts +2 -2
- package/dist/types/Server.d.ts +28 -10
- package/dist/types/auth/AuthMiddleware.d.ts +3 -3
- package/dist/types/auth/AuthStrategy.d.ts +10 -7
- package/dist/types/auth/JWTStrategy.d.ts +6 -4
- package/dist/types/auth/index.d.ts +0 -1
- package/dist/types/database/ConnectionKinds.d.ts +7 -0
- package/dist/types/database/ConnectionManager.d.ts +29 -4
- package/dist/types/database/MongoConnection.d.ts +16 -4
- package/dist/types/database/MongoRepository.d.ts +32 -11
- package/dist/types/database/RedisCache.d.ts +23 -0
- package/dist/types/database/TypeOrmSupport.d.ts +4 -4
- package/dist/types/database/index.d.ts +1 -0
- package/dist/types/decorators/DatabaseDecorators.d.ts +139 -8
- package/dist/types/decorators/DocDecorators.d.ts +11 -2
- package/dist/types/decorators/ModelDecorators.d.ts +12 -5
- package/dist/types/decorators/PersistenceDecorators.d.ts +11 -4
- package/dist/types/decorators/RouteDecorators.d.ts +36 -6
- package/dist/types/http/IWebSocketShim.d.ts +11 -0
- package/dist/types/http/MiddlewareChain.d.ts +47 -0
- package/dist/types/http/RuntimeDetect.d.ts +2 -0
- package/dist/types/http/bun/BunAdapters.d.ts +139 -0
- package/dist/types/http/bun/BunRouter.d.ts +79 -0
- package/dist/types/http/bun/BunWebSocket.d.ts +35 -0
- package/dist/types/http/index.d.ts +16 -6
- package/dist/types/http/session/RedisSessionStore.d.ts +15 -0
- package/dist/types/http/session/SessionManager.d.ts +35 -0
- package/dist/types/http/session/SessionStore.d.ts +12 -0
- package/dist/types/http/session/sessionMiddleware.d.ts +10 -0
- package/dist/types/http/types.d.ts +42 -3
- package/dist/types/http/uWS/Adapters.d.ts +129 -0
- package/dist/types/http/{Router.d.ts → uWS/Router.d.ts} +14 -38
- package/dist/types/http/{WebSocket.d.ts → uWS/WebSocket.d.ts} +6 -5
- package/dist/types/models/BaseEntity.d.ts +1 -1
- package/dist/types/models/ModelUtils.d.ts +47 -14
- package/dist/types/models/RecoverableBaseEntity.d.ts +3 -1
- package/dist/types/models/RepoUtils.d.ts +84 -14
- package/dist/types/routes/BaseAdminRoute.d.ts +65 -0
- package/dist/types/routes/BaseMetricsRoute.d.ts +37 -0
- package/dist/types/routes/BaseOpenAPIRoute.d.ts +33 -0
- package/dist/types/routes/BasePushRoute.d.ts +53 -0
- package/dist/types/routes/BaseStaticRoute.d.ts +44 -0
- package/dist/types/routes/BaseStatusRoute.d.ts +37 -0
- package/dist/types/routes/CRUDRoute.d.ts +57 -0
- package/dist/types/routes/ModelRoute.d.ts +39 -36
- package/dist/types/routes/RouteUtils.d.ts +19 -0
- package/dist/types/routes/index.d.ts +7 -4
- package/dist/types/security/ACLUtils.d.ts +94 -18
- package/dist/types/security/AccessControlList.d.ts +37 -59
- package/dist/types/security/AccessControlListMongo.d.ts +3 -8
- package/dist/types/security/AccessControlListSQL.d.ts +3 -8
- package/dist/types/security/BaseACLRoute.d.ts +73 -0
- package/dist/types/security/index.d.ts +3 -0
- package/dist/types/test/request.d.ts +18 -1
- package/package.json +135 -129
- package/tsconfig.export.json +17 -0
- package/dist/lib/auth/BasicStrategy.js +0 -113
- package/dist/lib/auth/BasicStrategy.js.map +0 -1
- package/dist/lib/http/Adapters.js +0 -241
- package/dist/lib/http/Adapters.js.map +0 -1
- package/dist/lib/http/Router.js.map +0 -1
- package/dist/lib/http/WebSocket.js.map +0 -1
- package/dist/lib/routes/AdminRoute.js.map +0 -1
- package/dist/lib/routes/MetricsRoute.js.map +0 -1
- package/dist/lib/routes/OpenAPIRoute.js.map +0 -1
- package/dist/lib/routes/StatusRoute.js +0 -55
- package/dist/lib/routes/StatusRoute.js.map +0 -1
- package/dist/lib/security/ACLRouteMongo.js +0 -194
- package/dist/lib/security/ACLRouteMongo.js.map +0 -1
- package/dist/lib/security/ACLRouteSQL.js +0 -193
- package/dist/lib/security/ACLRouteSQL.js.map +0 -1
- package/dist/types/auth/BasicStrategy.d.ts +0 -37
- package/dist/types/http/Adapters.d.ts +0 -68
- package/dist/types/routes/AdminRoute.d.ts +0 -47
- package/dist/types/routes/MetricsRoute.d.ts +0 -15
- package/dist/types/routes/OpenAPIRoute.d.ts +0 -17
- package/dist/types/routes/StatusRoute.d.ts +0 -11
- package/dist/types/security/ACLRouteMongo.d.ts +0 -19
- package/dist/types/security/ACLRouteSQL.d.ts +0 -19
- package/docs/Makefile +0 -20
- package/docs/conf.py +0 -58
- package/docs/index.rst +0 -17
- package/docs/make.bat +0 -35
- package/docs/reference/@rapidrest/namespaces/DatabaseDecorators/README.md +0 -13
- package/docs/reference/@rapidrest/namespaces/DatabaseDecorators/functions/MongoRepository.md +0 -25
- package/docs/reference/@rapidrest/namespaces/DatabaseDecorators/functions/RedisConnection.md +0 -25
- package/docs/reference/@rapidrest/namespaces/DatabaseDecorators/functions/Repository.md +0 -25
- package/docs/reference/@rapidrest/namespaces/DocDecorators/README.md +0 -23
- package/docs/reference/@rapidrest/namespaces/DocDecorators/functions/Default.md +0 -25
- package/docs/reference/@rapidrest/namespaces/DocDecorators/functions/Description.md +0 -25
- package/docs/reference/@rapidrest/namespaces/DocDecorators/functions/Document.md +0 -25
- package/docs/reference/@rapidrest/namespaces/DocDecorators/functions/Example.md +0 -25
- package/docs/reference/@rapidrest/namespaces/DocDecorators/functions/Format.md +0 -25
- package/docs/reference/@rapidrest/namespaces/DocDecorators/functions/Returns.md +0 -28
- package/docs/reference/@rapidrest/namespaces/DocDecorators/functions/Summary.md +0 -25
- package/docs/reference/@rapidrest/namespaces/DocDecorators/functions/Tags.md +0 -25
- package/docs/reference/@rapidrest/namespaces/DocDecorators/functions/TypeInfo.md +0 -28
- package/docs/reference/@rapidrest/namespaces/DocDecorators/interfaces/DocumentsData.md +0 -57
- package/docs/reference/@rapidrest/namespaces/EventDecorators/README.md +0 -12
- package/docs/reference/@rapidrest/namespaces/EventDecorators/functions/EventListener.md +0 -17
- package/docs/reference/@rapidrest/namespaces/EventDecorators/functions/OnEvent.md +0 -26
- package/docs/reference/@rapidrest/namespaces/ModelDecorators/README.md +0 -26
- package/docs/reference/@rapidrest/namespaces/ModelDecorators/functions/Cache.md +0 -25
- package/docs/reference/@rapidrest/namespaces/ModelDecorators/functions/ChildEntity.md +0 -18
- package/docs/reference/@rapidrest/namespaces/ModelDecorators/functions/DataStore.md +0 -25
- package/docs/reference/@rapidrest/namespaces/ModelDecorators/functions/Identifier.md +0 -27
- package/docs/reference/@rapidrest/namespaces/ModelDecorators/functions/Protect.md +0 -35
- package/docs/reference/@rapidrest/namespaces/ModelDecorators/functions/Reference.md +0 -25
- package/docs/reference/@rapidrest/namespaces/ModelDecorators/functions/Shard.md +0 -27
- package/docs/reference/@rapidrest/namespaces/ModelDecorators/functions/TrackChanges.md +0 -26
- package/docs/reference/@rapidrest/namespaces/ModelDecorators/interfaces/PendingTypeOrmColumn.md +0 -45
- package/docs/reference/@rapidrest/namespaces/ModelDecorators/variables/pendingTypeOrmColumns.md +0 -14
- package/docs/reference/@rapidrest/namespaces/PersistenceDecorators/README.md +0 -27
- package/docs/reference/@rapidrest/namespaces/PersistenceDecorators/functions/Column.md +0 -25
- package/docs/reference/@rapidrest/namespaces/PersistenceDecorators/functions/Entity.md +0 -25
- package/docs/reference/@rapidrest/namespaces/PersistenceDecorators/functions/Index.md +0 -119
- package/docs/reference/@rapidrest/namespaces/PersistenceDecorators/functions/PrimaryColumn.md +0 -25
- package/docs/reference/@rapidrest/namespaces/PersistenceDecorators/functions/Unique.md +0 -68
- package/docs/reference/@rapidrest/namespaces/PersistenceDecorators/functions/getColumnMetadata.md +0 -26
- package/docs/reference/@rapidrest/namespaces/PersistenceDecorators/functions/getEntityName.md +0 -26
- package/docs/reference/@rapidrest/namespaces/PersistenceDecorators/functions/getIndexMetadata.md +0 -27
- package/docs/reference/@rapidrest/namespaces/PersistenceDecorators/interfaces/CollationOptions.md +0 -79
- package/docs/reference/@rapidrest/namespaces/PersistenceDecorators/interfaces/ColumnInfo.md +0 -41
- package/docs/reference/@rapidrest/namespaces/PersistenceDecorators/interfaces/ColumnOptions.md +0 -51
- package/docs/reference/@rapidrest/namespaces/PersistenceDecorators/interfaces/EntityOptions.md +0 -32
- package/docs/reference/@rapidrest/namespaces/PersistenceDecorators/interfaces/IndexInfo.md +0 -41
- package/docs/reference/@rapidrest/namespaces/PersistenceDecorators/interfaces/IndexOptions.md +0 -61
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/README.md +0 -36
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/After.md +0 -26
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/Auth.md +0 -33
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/AuthResult.md +0 -31
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/Before.md +0 -25
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/ContentType.md +0 -25
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/Delete.md +0 -25
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/Get.md +0 -25
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/Head.md +0 -25
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/Header.md +0 -25
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/Method.md +0 -31
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/Model.md +0 -25
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/Options.md +0 -25
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/Param.md +0 -26
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/Patch.md +0 -25
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/Post.md +0 -25
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/Protect.md +0 -31
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/Put.md +0 -25
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/Query.md +0 -26
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/Request.md +0 -31
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/RequiresRole.md +0 -26
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/Response.md +0 -31
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/Route.md +0 -25
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/Socket.md +0 -33
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/User.md +0 -31
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/Validate.md +0 -25
- package/docs/reference/@rapidrest/namespaces/RouteDecorators/functions/WebSocket.md +0 -25
- package/docs/reference/README.md +0 -20
- package/docs/reference/classes/ACLUtils.md +0 -263
- package/docs/reference/classes/AdminRoute.md +0 -51
- package/docs/reference/classes/AuthMiddleware.md +0 -173
- package/docs/reference/classes/BackgroundService.md +0 -117
- package/docs/reference/classes/BackgroundServiceManager.md +0 -172
- package/docs/reference/classes/BaseEntity.md +0 -82
- package/docs/reference/classes/BaseMongoEntity.md +0 -107
- package/docs/reference/classes/BasicStrategy.md +0 -131
- package/docs/reference/classes/BasicStrategyOptions.md +0 -115
- package/docs/reference/classes/BulkError.md +0 -271
- package/docs/reference/classes/ConnectionManager.md +0 -75
- package/docs/reference/classes/EventListenerManager.md +0 -88
- package/docs/reference/classes/HttpRouter.md +0 -343
- package/docs/reference/classes/JWTStrategy.md +0 -100
- package/docs/reference/classes/JWTStrategyOptions.md +0 -97
- package/docs/reference/classes/MetricsRoute.md +0 -27
- package/docs/reference/classes/ModelRoute.md +0 -527
- package/docs/reference/classes/ModelUtils.md +0 -446
- package/docs/reference/classes/MongoConnection.md +0 -218
- package/docs/reference/classes/MongoRepository.md +0 -390
- package/docs/reference/classes/MongoSchemaSync.md +0 -97
- package/docs/reference/classes/NetUtils.md +0 -90
- package/docs/reference/classes/NotificationUtils.md +0 -77
- package/docs/reference/classes/ObjectFactory.md +0 -336
- package/docs/reference/classes/OpenAPIRoute.md +0 -77
- package/docs/reference/classes/OpenApiSpec.md +0 -892
- package/docs/reference/classes/RecoverableBaseEntity.md +0 -114
- package/docs/reference/classes/RecoverableBaseMongoEntity.md +0 -124
- package/docs/reference/classes/RedisTransport.md +0 -2202
- package/docs/reference/classes/RepoUtils.md +0 -482
- package/docs/reference/classes/RouteUtils.md +0 -191
- package/docs/reference/classes/Server.md +0 -408
- package/docs/reference/classes/SimpleEntity.md +0 -48
- package/docs/reference/classes/SimpleMongoEntity.md +0 -66
- package/docs/reference/classes/StatusExtraData.md +0 -57
- package/docs/reference/classes/StatusRoute.md +0 -26
- package/docs/reference/classes/UWSRequest.md +0 -226
- package/docs/reference/classes/UWSResponse.md +0 -257
- package/docs/reference/classes/UWSWebSocketShim.md +0 -958
- package/docs/reference/enumerations/ACLAction.md +0 -63
- package/docs/reference/enumerations/ApiErrorMessages.md +0 -123
- package/docs/reference/enumerations/ApiErrors.md +0 -123
- package/docs/reference/functions/createWebSocketStream.md +0 -27
- package/docs/reference/functions/isSqlDataSource.md +0 -26
- package/docs/reference/functions/readBody.md +0 -29
- package/docs/reference/functions/resolveCollectionName.md +0 -33
- package/docs/reference/functions/runChain.md +0 -40
- package/docs/reference/functions/snakeCase.md +0 -28
- package/docs/reference/globals.md +0 -106
- package/docs/reference/interfaces/ACLRecord.md +0 -96
- package/docs/reference/interfaces/AccessControlList.md +0 -76
- package/docs/reference/interfaces/AuthResult.md +0 -55
- package/docs/reference/interfaces/AuthStrategy.md +0 -95
- package/docs/reference/interfaces/CreateRequestOptions.md +0 -121
- package/docs/reference/interfaces/DeleteRequestOptions.md +0 -137
- package/docs/reference/interfaces/FindRequestOptions.md +0 -133
- package/docs/reference/interfaces/HttpRequest.md +0 -147
- package/docs/reference/interfaces/HttpResponse.md +0 -164
- package/docs/reference/interfaces/JWTAuthResult.md +0 -84
- package/docs/reference/interfaces/RepoCreateOptions.md +0 -95
- package/docs/reference/interfaces/RepoDeleteOptions.md +0 -105
- package/docs/reference/interfaces/RepoFindOptions.md +0 -125
- package/docs/reference/interfaces/RepoOperationOptions.md +0 -69
- package/docs/reference/interfaces/RepoUpdateOptions.md +0 -101
- package/docs/reference/interfaces/RequestOptions.md +0 -112
- package/docs/reference/interfaces/RequestWS.md +0 -225
- package/docs/reference/interfaces/TruncateRequestOptions.md +0 -161
- package/docs/reference/interfaces/UpdateRequestOptions.md +0 -139
- package/docs/reference/type-aliases/ErrorHandler.md +0 -35
- package/docs/reference/type-aliases/NextFunction.md +0 -23
- package/docs/reference/type-aliases/OneOrMany.md +0 -19
- package/docs/reference/type-aliases/OneOrNull.md +0 -19
- package/docs/reference/type-aliases/PartialBaseEntity.md +0 -17
- package/docs/reference/type-aliases/PartialSimpleEntity.md +0 -17
- package/docs/reference/type-aliases/RequestHandler.md +0 -31
- package/docs/reference/type-aliases/UpdateObject.md +0 -19
- package/docs/reference/type-aliases/WsUpgradeAuth.md +0 -27
- package/docs/reference/type-aliases/WsUpgradeAuthResult.md +0 -47
|
@@ -9,6 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
};
|
|
10
10
|
///////////////////////////////////////////////////////////////////////////////
|
|
11
11
|
// Copyright (C) 2020-2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
12
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
12
13
|
////////////////////////////////////////////////////////////////////////////////
|
|
13
14
|
import * as crypto from "crypto";
|
|
14
15
|
import { MongoRepository } from "../database/MongoRepository.js";
|
|
@@ -20,15 +21,13 @@ import { BaseEntity } from "../models/BaseEntity.js";
|
|
|
20
21
|
import { BaseMongoEntity } from "../models/BaseMongoEntity.js";
|
|
21
22
|
import { ApiErrorMessages, ApiErrors } from "../ApiErrors.js";
|
|
22
23
|
import { ApiError, ObjectDecorators, ObjectUtils, UserUtils } from "@rapidrest/core";
|
|
23
|
-
import { DatabaseDecorators } from "../decorators/index.js";
|
|
24
|
-
import { Redis } from "ioredis";
|
|
25
|
-
import { ObjectFactory } from "../ObjectFactory.js";
|
|
26
24
|
import { NotificationUtils } from "../NotificationUtils.js";
|
|
27
25
|
import { RecoverableBaseEntity } from "./RecoverableBaseEntity.js";
|
|
28
26
|
import { ACLAction } from "../security/index.js";
|
|
29
|
-
import { ConnectionManager } from "../database/index.js";
|
|
27
|
+
import { ConnectionManager, RedisCache } from "../database/index.js";
|
|
28
|
+
import { registerRollbackHook, Transactional, transactionContext } from "../decorators/DatabaseDecorators.js";
|
|
30
29
|
const { Config, Init, Inject, Logger } = ObjectDecorators;
|
|
31
|
-
const
|
|
30
|
+
const _hashCache = new Map();
|
|
32
31
|
/**
|
|
33
32
|
* @author Jean-Philippe Steinmetz
|
|
34
33
|
*/
|
|
@@ -43,18 +42,25 @@ export class RepoUtils {
|
|
|
43
42
|
async init() {
|
|
44
43
|
// Retrieve the repository based on the modelClass that was passed in to the constructor
|
|
45
44
|
if (!this.repo) {
|
|
46
|
-
if (!this.modelClass.
|
|
45
|
+
if (!this.modelClass.datasource) {
|
|
47
46
|
throw new Error(`Cannot initialize RepoUtils. Did you forget to add @DataStore() to ${this.modelClass.name}?`);
|
|
48
47
|
}
|
|
49
48
|
if (!this.connectionManager) {
|
|
50
49
|
throw new Error("Cannot initialize RepoUtils. Failed to retrieve ConnectionManager.");
|
|
51
50
|
}
|
|
52
|
-
const ds = this.connectionManager.connections.get(this.modelClass.
|
|
51
|
+
const ds = this.connectionManager.connections.get(this.modelClass.datasource);
|
|
53
52
|
if (!ds) {
|
|
54
|
-
throw new Error(`Cannot initialize RepoUtils. No connection found for
|
|
53
|
+
throw new Error(`Cannot initialize RepoUtils. No connection found for datasource '${this.modelClass.datasource}'`);
|
|
55
54
|
}
|
|
56
55
|
this.repo = ds.getRepository(this.modelClass);
|
|
57
56
|
}
|
|
57
|
+
// Create the cache store if caching is enabled for this entity type
|
|
58
|
+
if (!this.cache && this.modelClass.cacheTTL) {
|
|
59
|
+
this.cache = await this._objectFactory?.newInstance(RedisCache, {
|
|
60
|
+
name: this.modelClass.fqn ?? this.modelClass.name,
|
|
61
|
+
args: [this.modelClass],
|
|
62
|
+
});
|
|
63
|
+
}
|
|
58
64
|
if (!this.repo) {
|
|
59
65
|
throw new Error(`Cannot initialize RepoUtils. No repository found for class ${this.modelClass.name}.`);
|
|
60
66
|
}
|
|
@@ -68,11 +74,11 @@ export class RepoUtils {
|
|
|
68
74
|
// Does the model specify a MongoDB shard configuration?
|
|
69
75
|
const shardConfig = Reflect.getMetadata("rrst:shardConfig", this.modelClass);
|
|
70
76
|
if (shardConfig && this.repo instanceof MongoRepository) {
|
|
71
|
-
const conn = this.connectionManager?.connections.get(this.modelClass.
|
|
77
|
+
const conn = this.connectionManager?.connections.get(this.modelClass.datasource);
|
|
72
78
|
const admin = conn?.admin();
|
|
73
79
|
if (admin) {
|
|
74
80
|
const collectionName = resolveCollectionName(this.modelClass);
|
|
75
|
-
const dbName = this.config.get(`datastores:${this.modelClass.
|
|
81
|
+
const dbName = this.config.get(`datastores:${this.modelClass.datasource}:database`);
|
|
76
82
|
try {
|
|
77
83
|
this.logger.info(`Configuring sharding for: collection=${dbName}.${collectionName}, key=${JSON.stringify(shardConfig.key)}, unique=${shardConfig.unique}, options=${JSON.stringify(shardConfig.options)})`);
|
|
78
84
|
const result = await admin.command({
|
|
@@ -93,39 +99,203 @@ export class RepoUtils {
|
|
|
93
99
|
}
|
|
94
100
|
}
|
|
95
101
|
/**
|
|
96
|
-
*
|
|
102
|
+
* Retrieves every uid matching the given (already-built) search query, ignoring any pagination `take`/`page`
|
|
103
|
+
* baked into it by `ModelUtils.buildSearchQuery`. Used by `count`/`exists`/`truncate`, which must narrow by
|
|
104
|
+
* record-level ACLs against the *entire* matching set rather than a single page of it — applying the default
|
|
105
|
+
* `take` here would silently undercount, under-check existence for, or under-delete a large result set.
|
|
97
106
|
*/
|
|
98
|
-
|
|
99
|
-
|
|
107
|
+
async findAllUids(searchQuery, options) {
|
|
108
|
+
const txInfo = this.getTransaction(options);
|
|
109
|
+
if (this.repo instanceof MongoRepository) {
|
|
110
|
+
if (Array.isArray(searchQuery)) {
|
|
111
|
+
return await this.repo.distinct("uid", searchQuery[0].$match, { session: txInfo?.session });
|
|
112
|
+
}
|
|
113
|
+
return await this.repo.distinct("uid", searchQuery["$match"] ? searchQuery["$match"] : searchQuery, {
|
|
114
|
+
session: txInfo?.session,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// Only the uid column is needed, and pagination must not clip the result set here.
|
|
118
|
+
const uidQuery = { ...searchQuery, select: { uid: true } };
|
|
119
|
+
delete uidQuery.take;
|
|
120
|
+
delete uidQuery.page;
|
|
121
|
+
const repo = txInfo?.entityManager ? txInfo.entityManager.getRepository(this.modelClass) : this.repo;
|
|
122
|
+
const rows = (await repo.find(uidQuery));
|
|
123
|
+
return rows.map((obj) => obj.uid);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Filters the given uids down to those the user has `action` permission for, checking in bounded-size
|
|
127
|
+
* batches rather than a single unbounded `Promise.all` so a large matching set can't fire an unbounded
|
|
128
|
+
* number of concurrent permission-check round trips at once.
|
|
129
|
+
*/
|
|
130
|
+
async filterPermittedUids(uids, action, options) {
|
|
131
|
+
const batchSize = 100;
|
|
132
|
+
const permitted = [];
|
|
133
|
+
for (let i = 0; i < uids.length; i += batchSize) {
|
|
134
|
+
const batch = uids.slice(i, i + batchSize);
|
|
135
|
+
const results = await Promise.all(batch.map((uid) => this.aclUtils.hasPermission(options?.user, uid, action)));
|
|
136
|
+
for (let j = 0; j < batch.length; j++) {
|
|
137
|
+
if (results[j]) {
|
|
138
|
+
permitted.push(batch[j]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return permitted;
|
|
100
143
|
}
|
|
101
144
|
async count(query, options) {
|
|
102
145
|
if (!this.repo) {
|
|
103
146
|
throw new ApiError(ApiErrors.INTERNAL_ERROR, 500, ApiErrorMessages.INTERNAL_ERROR);
|
|
104
147
|
}
|
|
105
148
|
let count = 0;
|
|
106
|
-
|
|
149
|
+
const action = options?.action ?? ACLAction.COUNT;
|
|
150
|
+
const txInfo = this.getTransaction(options);
|
|
151
|
+
// Check user permissions against the class-level ACL. This is a fast-fail gate for users with no
|
|
152
|
+
// legitimate access to the resource type at all; per-record narrowing (below) is an additional layer
|
|
153
|
+
// on top of this, not a replacement for it.
|
|
107
154
|
if (this.aclUtils?.enabled && !options?.ignoreACL) {
|
|
108
|
-
if (!(await this.aclUtils.hasPermission(options?.user, this.defaultACLUid,
|
|
155
|
+
if (!(await this.aclUtils.hasPermission(options?.user, this.defaultACLUid, action))) {
|
|
109
156
|
throw new ApiError(ApiErrors.AUTH_PERMISSION_FAILURE, 403, ApiErrorMessages.AUTH_PERMISSION_FAILURE);
|
|
110
157
|
}
|
|
111
158
|
}
|
|
159
|
+
// A client-supplied `?deleted=true` filter overrides `buildSearchQuery()`'s default exclusion of
|
|
160
|
+
// soft-deleted rows. Counting a matched soft-deleted row requires the DELETE+UPDATE permissions.
|
|
161
|
+
const clientRequestsDeleted = query?.deleted === true || query?.deleted === "true";
|
|
162
|
+
const recordACL = !!this.modelClass.recordACL;
|
|
163
|
+
let effectiveQuery = query;
|
|
164
|
+
if (clientRequestsDeleted && this.aclUtils?.enabled && !options?.ignoreACL && !recordACL) {
|
|
165
|
+
if (!(await this.canViewDeleted(options?.user, this.defaultACLUid))) {
|
|
166
|
+
effectiveQuery = { ...query };
|
|
167
|
+
delete effectiveQuery.deleted;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const searchQuery = ModelUtils.buildSearchQuery(this.modelClass, this.repo, effectiveQuery, true, options?.user);
|
|
171
|
+
// `buildSearchQuery()` auto-excludes soft-deleted rows for a RecoverableBaseEntity by default. We strip
|
|
172
|
+
// that out of the query rather than trying to influence the exclusion via the input `query` object.
|
|
173
|
+
if (options?.includeDeleted) {
|
|
174
|
+
if (Array.isArray(searchQuery)) {
|
|
175
|
+
delete searchQuery[0]?.$match?.deleted;
|
|
176
|
+
}
|
|
177
|
+
else if (searchQuery?.$match) {
|
|
178
|
+
delete searchQuery.$match.deleted;
|
|
179
|
+
}
|
|
180
|
+
else if (Array.isArray(searchQuery?.where)) {
|
|
181
|
+
for (const w of searchQuery.where) {
|
|
182
|
+
delete w.deleted;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Record-level ACLs aren't reflected in the query itself, so the matched uids must be checked
|
|
187
|
+
// individually and counted rather than delegating the count to the database.
|
|
188
|
+
if (this.aclUtils?.enabled && !options?.ignoreACL && recordACL) {
|
|
189
|
+
const uids = await this.findAllUids(searchQuery, options);
|
|
190
|
+
let permitted;
|
|
191
|
+
if (clientRequestsDeleted) {
|
|
192
|
+
// Every uid this query matched is, by construction, a soft-deleted record - check the restore
|
|
193
|
+
// bar per-record rather than the ordinary `action`.
|
|
194
|
+
const deleteOk = new Set(await this.filterPermittedUids(uids, ACLAction.DELETE, options));
|
|
195
|
+
const updateOk = await this.filterPermittedUids(uids, ACLAction.UPDATE, options);
|
|
196
|
+
permitted = updateOk.filter((uid) => deleteOk.has(uid));
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
permitted = await this.filterPermittedUids(uids, action, options);
|
|
200
|
+
}
|
|
201
|
+
return permitted.length;
|
|
202
|
+
}
|
|
112
203
|
if (this.repo instanceof MongoRepository) {
|
|
113
|
-
if (Array.isArray(
|
|
114
|
-
|
|
115
|
-
const result = await this.repo.aggregate(
|
|
204
|
+
if (Array.isArray(searchQuery)) {
|
|
205
|
+
searchQuery.push({ $count: "count" });
|
|
206
|
+
const result = await this.repo.aggregate(searchQuery, { session: txInfo?.session }).next();
|
|
116
207
|
count = result ? result.count : count;
|
|
117
208
|
}
|
|
118
209
|
else {
|
|
119
|
-
|
|
210
|
+
const repo = txInfo?.entityManager ? txInfo.entityManager.getRepository(this.modelClass) : this.repo;
|
|
211
|
+
count = await repo.count(searchQuery["$match"] ? searchQuery["$match"] : searchQuery, {
|
|
212
|
+
session: txInfo?.session,
|
|
213
|
+
});
|
|
120
214
|
}
|
|
121
215
|
}
|
|
122
216
|
else {
|
|
123
|
-
|
|
217
|
+
const repo = txInfo?.entityManager ? txInfo.entityManager.getRepository(this.modelClass) : this.repo;
|
|
218
|
+
count = await repo.count(searchQuery);
|
|
124
219
|
}
|
|
125
220
|
return count;
|
|
126
221
|
}
|
|
127
222
|
/**
|
|
128
|
-
*
|
|
223
|
+
* Determines whether an object with the given unique identifier (and, optionally, a specific version) exists
|
|
224
|
+
* in the datasource. Respects record-level ACLs the same way `count()` does.
|
|
225
|
+
*
|
|
226
|
+
* @param id The unique identifier of the object to check for.
|
|
227
|
+
* @param options The additional options to consider, such as `version` and the requesting `user`.
|
|
228
|
+
*/
|
|
229
|
+
async exists(id, options) {
|
|
230
|
+
if (!this.repo) {
|
|
231
|
+
throw new ApiError(ApiErrors.INTERNAL_ERROR, 500, ApiErrorMessages.INTERNAL_ERROR);
|
|
232
|
+
}
|
|
233
|
+
const action = options?.action ?? ACLAction.EXISTS;
|
|
234
|
+
// Check user permissions against the class-level ACL. This is a fast-fail gate for users with no
|
|
235
|
+
// legitimate access to the resource type at all; per-record narrowing (below) is an additional layer
|
|
236
|
+
// on top of this, not a replacement for it.
|
|
237
|
+
if (this.aclUtils?.enabled && !options?.ignoreACL) {
|
|
238
|
+
if (!(await this.aclUtils.hasPermission(options?.user, this.defaultACLUid, action))) {
|
|
239
|
+
throw new ApiError(ApiErrors.AUTH_PERMISSION_FAILURE, 403, ApiErrorMessages.AUTH_PERMISSION_FAILURE);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Ordinary (live-record) existence check, respecting `action`/record-level ACLs exactly as before.
|
|
243
|
+
let count = await this.existsQuery(id, options, false, action);
|
|
244
|
+
// A soft-deleted record only counts as "existing" for a caller with both DELETE and UPDATE permission.
|
|
245
|
+
if (count === 0 && options?.includeDeleted) {
|
|
246
|
+
let canRestore = true;
|
|
247
|
+
if (this.aclUtils?.enabled && !options?.ignoreACL) {
|
|
248
|
+
const restoreAclUid = this.modelClass.recordACL ? id : this.defaultACLUid;
|
|
249
|
+
canRestore = await this.canViewDeleted(options?.user, restoreAclUid);
|
|
250
|
+
}
|
|
251
|
+
if (canRestore) {
|
|
252
|
+
// Permission for the deleted record was already established above, so this pass runs as a raw
|
|
253
|
+
// existence check rather than re-deriving/re-checking `action` (which the record's ACL may not
|
|
254
|
+
// grant even to someone who can restore it).
|
|
255
|
+
count = await this.existsQuery(id, options, true, null);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return count;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Runs the actual existence check/count for `exists()`, deduped by uid and clamped to at most 1. Split out
|
|
262
|
+
* so `exists()` can run it twice — once for a live record, once (gated on DELETE+UPDATE permission) for a
|
|
263
|
+
* soft-deleted one — without duplicating the Mongo/SQL/record-ACL branching.
|
|
264
|
+
*
|
|
265
|
+
* @param id The unique identifier of the object to check for.
|
|
266
|
+
* @param options The additional options to consider, such as `version`.
|
|
267
|
+
* @param includeDeleted Whether to match a soft-deleted record.
|
|
268
|
+
* @param enforceAction The ACL action to check per matched record on a `recordACL` model, or `null` to skip
|
|
269
|
+
* that check (used for the second, already-authorized `includeDeleted` pass).
|
|
270
|
+
*/
|
|
271
|
+
async existsQuery(id, options, includeDeleted, enforceAction) {
|
|
272
|
+
if (!this.repo) {
|
|
273
|
+
throw new ApiError(ApiErrors.INTERNAL_ERROR, 500, ApiErrorMessages.INTERNAL_ERROR);
|
|
274
|
+
}
|
|
275
|
+
const txInfo = this.getTransaction(options);
|
|
276
|
+
// Without an explicit version, `query` matches every historical row sharing this uid on a trackChanges
|
|
277
|
+
// entity - existence is still a yes/no question about the uid itself, so results are deduped by uid and
|
|
278
|
+
// the final count clamped to at most 1, rather than reporting the number of matching version rows.
|
|
279
|
+
const query = this.searchIdQuery(id, options?.version, includeDeleted);
|
|
280
|
+
// Record-level ACLs aren't reflected in the query itself, so the matched uids must be checked
|
|
281
|
+
// individually and counted rather than delegating the count to the database.
|
|
282
|
+
if (enforceAction && this.aclUtils?.enabled && !options?.ignoreACL && this.modelClass.recordACL) {
|
|
283
|
+
const uids = await this.findAllUids(query);
|
|
284
|
+
const permitted = await this.filterPermittedUids(uids, enforceAction, options);
|
|
285
|
+
return permitted.length > 0 ? 1 : 0;
|
|
286
|
+
}
|
|
287
|
+
let count;
|
|
288
|
+
if (this.repo instanceof MongoRepository) {
|
|
289
|
+
count = await this.repo.count(query, { session: txInfo?.session });
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
const repo = txInfo?.entityManager ? txInfo.entityManager.getRepository(this.modelClass) : this.repo;
|
|
293
|
+
count = await repo.count(query);
|
|
294
|
+
}
|
|
295
|
+
return Math.min(count, 1);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Stores a new record of the provided object in the datasource. Performs pre-processing, permission checks against
|
|
129
299
|
* the class ACL, cache seeding, telemetry recording and push notifications.
|
|
130
300
|
*
|
|
131
301
|
* @param obj The object to store.
|
|
@@ -135,6 +305,7 @@ export class RepoUtils {
|
|
|
135
305
|
if (!this.repo) {
|
|
136
306
|
throw new ApiError(ApiErrors.INTERNAL_ERROR, 500, ApiErrorMessages.INTERNAL_ERROR);
|
|
137
307
|
}
|
|
308
|
+
const txInfo = this.getTransaction(options);
|
|
138
309
|
// Verify the user's permission to create objects
|
|
139
310
|
if (this.aclUtils?.enabled &&
|
|
140
311
|
!options?.ignoreACL &&
|
|
@@ -144,6 +315,7 @@ export class RepoUtils {
|
|
|
144
315
|
// Instantiate the object if not already done
|
|
145
316
|
const clazz = this.getClassType(obj);
|
|
146
317
|
const newObj = obj instanceof clazz ? obj : this.instantiateObject(obj, clazz);
|
|
318
|
+
const repo = this.repo;
|
|
147
319
|
// Make sure an existing object doesn't already exist with the same identifiers
|
|
148
320
|
const ids = [];
|
|
149
321
|
const idProps = ModelUtils.getIdPropertyNames(clazz);
|
|
@@ -153,11 +325,28 @@ export class RepoUtils {
|
|
|
153
325
|
ids.push(val);
|
|
154
326
|
}
|
|
155
327
|
}
|
|
156
|
-
const query = ModelUtils.buildIdSearchQuery(
|
|
157
|
-
const count =
|
|
328
|
+
const query = ModelUtils.buildIdSearchQuery(repo, clazz, ids, undefined);
|
|
329
|
+
const count = this.repo instanceof MongoRepository
|
|
330
|
+
? await this.repo.count(query, { session: txInfo?.session })
|
|
331
|
+
: await (txInfo?.entityManager ? txInfo.entityManager.getRepository(this.modelClass) : this.repo).count(query);
|
|
158
332
|
if (!this.modelClass.trackChanges && count > 0) {
|
|
159
333
|
throw new ApiError(ApiErrors.IDENTIFIER_EXISTS, 400, ApiErrorMessages.IDENTIFIER_EXISTS);
|
|
160
334
|
}
|
|
335
|
+
else if (this.modelClass.trackChanges &&
|
|
336
|
+
count > 0 &&
|
|
337
|
+
this.modelClass.recordACL &&
|
|
338
|
+
this.aclUtils?.enabled &&
|
|
339
|
+
!(await this.aclUtils.hasPermission(options?.user, newObj.uid, ACLAction.UPDATE))) {
|
|
340
|
+
// A trackChanges + recordACL model is being "re-created" under an existing uid (i.e. a new
|
|
341
|
+
// version). That's only legitimate for someone who already has update rights on the
|
|
342
|
+
// existing record — generic class-level CREATE permission isn't enough, otherwise any
|
|
343
|
+
// creator could inject a new "latest version" of another user's record. Deliberately NOT
|
|
344
|
+
// gated on `options.ignoreACL`: that flag exists so ModelRoute.doCreate() can skip
|
|
345
|
+
// re-doing the class-level CREATE check it already performed upstream — it says nothing
|
|
346
|
+
// about this distinct, additional per-record check, which has no upstream equivalent and
|
|
347
|
+
// must always run.
|
|
348
|
+
throw new ApiError(ApiErrors.AUTH_PERMISSION_FAILURE, 403, ApiErrorMessages.AUTH_PERMISSION_FAILURE);
|
|
349
|
+
}
|
|
161
350
|
// Override the date and version fields with their defaults
|
|
162
351
|
if (newObj instanceof BaseEntity) {
|
|
163
352
|
newObj.dateCreated = new Date();
|
|
@@ -168,38 +357,85 @@ export class RepoUtils {
|
|
|
168
357
|
if (newObj instanceof BaseEntity && this.modelClass.trackChanges === 0) {
|
|
169
358
|
newObj.version = 0;
|
|
170
359
|
}
|
|
171
|
-
// HAX We shouldn't be casting obj to any here but this is the only way to get it to compile
|
|
172
|
-
// extends BaseEntity.
|
|
173
|
-
|
|
174
|
-
if (this.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
360
|
+
// HAX We shouldn't be casting obj to any here but this is the only way to get it to compile
|
|
361
|
+
// since T extends BaseEntity.
|
|
362
|
+
let saved;
|
|
363
|
+
if (this.repo instanceof MongoRepository) {
|
|
364
|
+
saved = await this.repo.save(newObj, { session: txInfo?.session });
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
const repo = txInfo?.entityManager ? txInfo.entityManager.getRepository(this.modelClass) : this.repo;
|
|
368
|
+
saved = await repo.save(newObj);
|
|
179
369
|
}
|
|
370
|
+
const result = this.instantiateObject(saved);
|
|
180
371
|
if (this.aclUtils?.enabled && this.modelClass.recordACL) {
|
|
181
|
-
//
|
|
182
|
-
|
|
372
|
+
// Reuse the existing ACL if this is a legitimate trackChanges "new version" rather than building a fresh,
|
|
373
|
+
// version-less object from scratch. Building fresh here would let saveACL()'s optimistic-lock version
|
|
374
|
+
// check pass by coincidence (a never-updated ACL is also version 0), silently discarding the real records.
|
|
375
|
+
const existingAcl = await this.aclUtils.findACL(result.uid);
|
|
376
|
+
const isFreshAcl = !existingAcl;
|
|
377
|
+
const acl = existingAcl ?? {
|
|
183
378
|
uid: result.uid,
|
|
184
379
|
parentUid: options?.acl?.parentUid || this.defaultACLUid,
|
|
185
380
|
records: options?.acl?.records || [],
|
|
186
381
|
};
|
|
187
382
|
// Look for an existing record for the creator
|
|
188
383
|
let found = !!this.aclUtils.getRecord(acl, options?.user);
|
|
384
|
+
let modifiedExistingAcl = false;
|
|
189
385
|
// Always grant the creator CRUD access, unless the user is a superuser.
|
|
190
386
|
if (!found && options?.user && !UserUtils.hasRoles(options?.user, this.trustedRoles)) {
|
|
191
387
|
acl.records.push({
|
|
192
388
|
userOrRoleId: options.user.uid,
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
389
|
+
actions: [
|
|
390
|
+
ACLAction.COUNT,
|
|
391
|
+
ACLAction.CREATE,
|
|
392
|
+
ACLAction.DELETE,
|
|
393
|
+
ACLAction.EXISTS,
|
|
394
|
+
ACLAction.READ,
|
|
395
|
+
ACLAction.LIST,
|
|
396
|
+
ACLAction.TRUNCATE,
|
|
397
|
+
ACLAction.UPDATE,
|
|
398
|
+
],
|
|
199
399
|
});
|
|
400
|
+
modifiedExistingAcl = !isFreshAcl;
|
|
200
401
|
}
|
|
201
402
|
await this.aclUtils.saveACL(acl);
|
|
403
|
+
// `saveACL()` commits independently, on the `acl` connection's own transaction (see its doc
|
|
404
|
+
// comment). That means it can't be rolled back by this (the entity-side) transaction's own abort if this
|
|
405
|
+
// transaction fails later. Register a compensating action so a later failure doesn't leave an orphaned
|
|
406
|
+
// ACL behind.
|
|
407
|
+
if (isFreshAcl) {
|
|
408
|
+
const newAclUid = acl.uid;
|
|
409
|
+
registerRollbackHook(async () => {
|
|
410
|
+
try {
|
|
411
|
+
await this.aclUtils.removeACL(newAclUid);
|
|
412
|
+
}
|
|
413
|
+
catch (err) {
|
|
414
|
+
this.logger?.warn(`RepoUtils: Failed to roll back orphaned ACL ${newAclUid} after a failed create().`);
|
|
415
|
+
this.logger?.debug(err);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
else if (modifiedExistingAcl) {
|
|
420
|
+
// Reverting a change to an already-existing ACL isn't well-defined in general (it may carry
|
|
421
|
+
// other, unrelated state) — log loudly instead so this is visible for manual reconciliation.
|
|
422
|
+
const modifiedAclUid = acl.uid;
|
|
423
|
+
registerRollbackHook(async () => {
|
|
424
|
+
this.logger?.warn(`RepoUtils: create() failed after modifying existing ACL ${modifiedAclUid} — that change was not automatically reverted.`);
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (this.cache) {
|
|
429
|
+
// Cache the object for faster retrieval.
|
|
430
|
+
const query = this.searchIdQuery(newObj.uid);
|
|
431
|
+
const cacheKey = this.hashQuery(query);
|
|
432
|
+
this.cache.save(cacheKey, result).catch((err) => this.logCacheError("save", err));
|
|
433
|
+
this.cache.save(result.uid, result).catch((err) => this.logCacheError("save", err));
|
|
202
434
|
}
|
|
435
|
+
// Process the result to remove any properties that have been scoped with @RequiresScope that the user
|
|
436
|
+
// does not have access to. Done after caching (the cache must retain the full object for other
|
|
437
|
+
// requests) but before the result is returned or broadcast to push subscribers.
|
|
438
|
+
ObjectUtils.deleteScopedProps(result, options?.user, this.modelClass);
|
|
203
439
|
if (!options?.skipPush) {
|
|
204
440
|
let channels = [result.uid].concat(options?.pushChannels || []);
|
|
205
441
|
this.notificationUtils?.sendMessage(channels, this.modelClass.name, "create", result);
|
|
@@ -210,6 +446,7 @@ export class RepoUtils {
|
|
|
210
446
|
if (!this.repo) {
|
|
211
447
|
throw new ApiError(ApiErrors.INTERNAL_ERROR, 500, ApiErrorMessages.INTERNAL_ERROR);
|
|
212
448
|
}
|
|
449
|
+
const txInfo = this.getTransaction(options);
|
|
213
450
|
if (this.aclUtils?.enabled && !options.ignoreACL) {
|
|
214
451
|
const acl = await this.aclUtils.findACL(uid);
|
|
215
452
|
if (!(await this.aclUtils.hasPermission(options.user, acl ? acl : this.defaultACLUid, ACLAction.DELETE))) {
|
|
@@ -218,18 +455,38 @@ export class RepoUtils {
|
|
|
218
455
|
}
|
|
219
456
|
const isRecoverable = this.instantiateObject({}) instanceof RecoverableBaseEntity;
|
|
220
457
|
const isPurge = isRecoverable ? options.purge || false : true;
|
|
458
|
+
// Delete must be able to target a record regardless of its current `deleted` state (the default) —
|
|
459
|
+
// otherwise an already soft-deleted record could never be purged, nor a soft-delete repeated idempotently.
|
|
221
460
|
const query = ModelUtils.buildIdSearchQuery(this.repo, this.modelClass, uid, options.version ? Number(options.version) : undefined);
|
|
222
461
|
// If the object(s) are being permenantly removed from the database do so and then clear the accompanying
|
|
223
462
|
// ACL(s). If the class type is recoverable and purge isn't desired, simply mark the object(s) as deleted.
|
|
224
463
|
if (isPurge) {
|
|
225
464
|
if (this.repo instanceof MongoRepository) {
|
|
226
|
-
await this.repo.deleteMany(query);
|
|
465
|
+
await this.repo.deleteMany(query, { session: txInfo?.session });
|
|
227
466
|
}
|
|
228
467
|
else {
|
|
229
|
-
|
|
468
|
+
const repo = txInfo?.entityManager ? txInfo.entityManager.getRepository(this.modelClass) : this.repo;
|
|
469
|
+
await repo.delete(query.where);
|
|
230
470
|
}
|
|
231
471
|
if (this.aclUtils?.enabled && this.modelClass.recordACL) {
|
|
232
|
-
|
|
472
|
+
// `removeACL()` returns the exact document it deleted (captured atomically, not via a separate
|
|
473
|
+
// earlier read - see its doc comment) - used as the restore snapshot below. `removeACL()`
|
|
474
|
+
// commits independently, on the `acl` connection's own transaction; if this (the entity-side)
|
|
475
|
+
// transaction later fails, its own abort can't undo that removal, so the rollback hook restores
|
|
476
|
+
// the snapshot in that case. `preserveVersion` restores its exact prior version instead of
|
|
477
|
+
// bumping it, and refuses (rather than clobbers) if something already exists at this uid.
|
|
478
|
+
const removedAcl = await this.aclUtils.removeACL(uid);
|
|
479
|
+
if (removedAcl) {
|
|
480
|
+
registerRollbackHook(async () => {
|
|
481
|
+
try {
|
|
482
|
+
await this.aclUtils.saveACL(removedAcl, { preserveVersion: true });
|
|
483
|
+
}
|
|
484
|
+
catch (err) {
|
|
485
|
+
this.logger?.warn(`RepoUtils: Failed to restore ACL ${uid} after a failed delete().`);
|
|
486
|
+
this.logger?.debug(err);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
}
|
|
233
490
|
}
|
|
234
491
|
}
|
|
235
492
|
else {
|
|
@@ -238,18 +495,22 @@ export class RepoUtils {
|
|
|
238
495
|
$set: {
|
|
239
496
|
deleted: true,
|
|
240
497
|
},
|
|
241
|
-
});
|
|
498
|
+
}, { session: txInfo?.session });
|
|
242
499
|
}
|
|
243
500
|
else {
|
|
244
|
-
|
|
501
|
+
const repo = txInfo?.entityManager ? txInfo.entityManager.getRepository(this.modelClass) : this.repo;
|
|
502
|
+
await repo.update(query.where, {
|
|
245
503
|
deleted: true,
|
|
246
504
|
});
|
|
247
505
|
}
|
|
248
506
|
}
|
|
249
|
-
if (this.
|
|
250
|
-
// Delete the object from cache
|
|
251
|
-
|
|
252
|
-
|
|
507
|
+
if (this.cache) {
|
|
508
|
+
// Delete the object from cache.
|
|
509
|
+
this.cache.delete(uid).catch((err) => this.logCacheError("delete", err));
|
|
510
|
+
this.cache.delete(this.hashQuery(query)).catch((err) => this.logCacheError("delete", err));
|
|
511
|
+
this.cache
|
|
512
|
+
.delete(this.hashQuery(this.searchIdQuery(uid)))
|
|
513
|
+
.catch((err) => this.logCacheError("delete", err));
|
|
253
514
|
}
|
|
254
515
|
if (!options?.skipPush) {
|
|
255
516
|
let channels = [uid].concat(options?.pushChannels || []);
|
|
@@ -260,7 +521,7 @@ export class RepoUtils {
|
|
|
260
521
|
}
|
|
261
522
|
}
|
|
262
523
|
/**
|
|
263
|
-
* Retrieves an array of objects from the
|
|
524
|
+
* Retrieves an array of objects from the datasource matching the given search query. This function will first
|
|
264
525
|
* attempt to look up the results in the cache. Also checks ACLs for READ permission.
|
|
265
526
|
*
|
|
266
527
|
* @param query The constructed search query to run.
|
|
@@ -270,9 +531,13 @@ export class RepoUtils {
|
|
|
270
531
|
if (!this.repo) {
|
|
271
532
|
throw new ApiError(ApiErrors.INTERNAL_ERROR, 500, ApiErrorMessages.INTERNAL_ERROR);
|
|
272
533
|
}
|
|
273
|
-
|
|
534
|
+
const action = options?.action ?? ACLAction.LIST;
|
|
535
|
+
const txInfo = this.getTransaction(options);
|
|
536
|
+
// Check user permissions against the class-level ACL. This is a fast-fail gate for users with no
|
|
537
|
+
// legitimate access to the resource type at all; per-record narrowing (below, right before the
|
|
538
|
+
// results are returned) is an additional layer on top of this, not a replacement for it.
|
|
274
539
|
if (this.aclUtils?.enabled && !options?.ignoreACL) {
|
|
275
|
-
if (!(await this.aclUtils.hasPermission(options?.user, this.defaultACLUid,
|
|
540
|
+
if (!(await this.aclUtils.hasPermission(options?.user, this.defaultACLUid, action))) {
|
|
276
541
|
throw new ApiError(ApiErrors.AUTH_PERMISSION_FAILURE, 403, ApiErrorMessages.AUTH_PERMISSION_FAILURE);
|
|
277
542
|
}
|
|
278
543
|
}
|
|
@@ -287,98 +552,74 @@ export class RepoUtils {
|
|
|
287
552
|
page,
|
|
288
553
|
});
|
|
289
554
|
// Pull from the cache if available
|
|
290
|
-
if (!options?.skipCache && this.
|
|
291
|
-
const
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
const uids = JSON.parse(json);
|
|
295
|
-
// Attempt to retrieve as many objects from the cache simultaneously
|
|
296
|
-
const keys = uids.map((uid) => `${this.baseCacheKey}.${this.hashQuery(this.searchIdQuery(uid))}`);
|
|
297
|
-
// Retrieve all objects from the cache (if possible)
|
|
298
|
-
const cresults = await this.cacheClient.mget(...keys);
|
|
299
|
-
// Check for any missing objects that weren't in the cache. Build a list of missing
|
|
300
|
-
// items that we can retrieve from the database directly.
|
|
301
|
-
const missingUids = [];
|
|
302
|
-
for (let i = 0; i < cresults.length; i++) {
|
|
303
|
-
if (cresults[i] === null) {
|
|
304
|
-
missingUids.push(uids[i]);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
// Now pull all missing items from the database
|
|
308
|
-
let missing = [];
|
|
309
|
-
if (missingUids.length > 0) {
|
|
310
|
-
if (this.repo instanceof MongoRepository) {
|
|
311
|
-
missing = await this.repo.find({ uid: { $in: missingUids } }).toArray();
|
|
312
|
-
}
|
|
313
|
-
else {
|
|
314
|
-
const { In } = ModelUtils.orm;
|
|
315
|
-
missing = await this.repo.find({ where: { uid: In(missingUids) } });
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
// Merge the results back into the results array. We iterate through cresults to preserve
|
|
319
|
-
// the order of the original query.
|
|
320
|
-
for (let i = 0; i < cresults.length; i++) {
|
|
321
|
-
if (cresults[i] !== null) {
|
|
322
|
-
results.push(JSON.parse(cresults[i]));
|
|
323
|
-
}
|
|
324
|
-
else {
|
|
325
|
-
// Find the desired object in the missing array
|
|
326
|
-
for (const obj of missing) {
|
|
327
|
-
if (obj.uid === missingUids[i]) {
|
|
328
|
-
results.push(obj);
|
|
329
|
-
break;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
catch (err) {
|
|
336
|
-
// It doesn't matter if this fails
|
|
337
|
-
}
|
|
555
|
+
if (!options?.skipCache && this.cache) {
|
|
556
|
+
const cached = await this.cache.loadSet(searchQueryHash);
|
|
557
|
+
if (cached) {
|
|
558
|
+
results = cached.filter((obj) => obj !== undefined);
|
|
338
559
|
}
|
|
339
560
|
}
|
|
340
561
|
// If the query wasn't cached retrieve from the database
|
|
341
562
|
if (results.length === 0) {
|
|
563
|
+
const searchQuery = ModelUtils.buildSearchQuery(this.modelClass, this.repo, query, true, options?.user);
|
|
342
564
|
if (this.repo instanceof MongoRepository) {
|
|
343
565
|
const skip = page * limit;
|
|
344
|
-
if (Array.isArray(
|
|
345
|
-
results = await this.repo
|
|
566
|
+
if (Array.isArray(searchQuery)) {
|
|
567
|
+
results = await this.repo
|
|
568
|
+
.aggregate(searchQuery, { session: txInfo?.session })
|
|
569
|
+
.skip(skip)
|
|
570
|
+
.limit(limit)
|
|
571
|
+
.toArray();
|
|
346
572
|
}
|
|
347
573
|
else {
|
|
348
574
|
results = await this.repo
|
|
349
|
-
.find(
|
|
575
|
+
.find(searchQuery["$match"] ? searchQuery["$match"] : searchQuery, {
|
|
350
576
|
limit,
|
|
577
|
+
session: txInfo?.session,
|
|
351
578
|
skip,
|
|
352
|
-
sort:
|
|
579
|
+
sort: searchQuery["$sort"],
|
|
353
580
|
})
|
|
354
581
|
.toArray();
|
|
355
582
|
}
|
|
356
583
|
}
|
|
357
584
|
else {
|
|
358
|
-
|
|
585
|
+
const repo = txInfo?.entityManager ? txInfo.entityManager.getRepository(this.modelClass) : this.repo;
|
|
586
|
+
// `searchQuery.page` (set by `buildSearchQuerySQL`) isn't a TypeORM find option and is silently
|
|
587
|
+
// ignored by `repo.find()` - it must be translated to `skip` here, the same way the Mongo branch
|
|
588
|
+
// above translates `page` into its own `skip`, or every SQL page request returns page 0.
|
|
589
|
+
results = (await repo.find({ ...searchQuery, skip: page * limit }));
|
|
359
590
|
}
|
|
360
591
|
// Cache the results for future requests. Don't bother if there were no results.
|
|
361
|
-
if (results.length > 0 && this.
|
|
362
|
-
|
|
363
|
-
//
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
cmds.push([
|
|
367
|
-
"setex",
|
|
368
|
-
`${this.baseCacheKey}.${searchQueryHash}`,
|
|
369
|
-
this.modelClass.cacheTTL,
|
|
370
|
-
JSON.stringify(uids),
|
|
371
|
-
]);
|
|
372
|
-
// Also seed individual object caches so the next request is fully cache-warm
|
|
373
|
-
for (const obj of results) {
|
|
374
|
-
const query = this.searchIdQuery(obj.uid);
|
|
375
|
-
const cacheKey = `${this.baseCacheKey}.${this.hashQuery(query)}`;
|
|
376
|
-
cmds.push(["setex", cacheKey, this.modelClass.cacheTTL, JSON.stringify(obj)]);
|
|
377
|
-
}
|
|
378
|
-
// Now send all commands to redis at once
|
|
379
|
-
void this.cacheClient.multi(cmds);
|
|
592
|
+
if (results.length > 0 && this.cache) {
|
|
593
|
+
this.cache.saveSet(searchQueryHash, results).catch((err) => this.logCacheError("saveSet", err));
|
|
594
|
+
// Also seed each individual object's own cache entry.
|
|
595
|
+
const ids = results.map((obj) => this.hashQuery(this.searchIdQuery(obj.uid)));
|
|
596
|
+
this.cache.saveMany(ids, results).catch((err) => this.logCacheError("saveMany", err));
|
|
380
597
|
}
|
|
381
598
|
}
|
|
599
|
+
// Record-level ACLs aren't reflected in the query itself (nor in cached results, which are shared across
|
|
600
|
+
// users), so each matched record must be checked individually before it's returned to the caller. The
|
|
601
|
+
// checks are run concurrently, and share a request-scoped ACL cache, so that a page of N results costs
|
|
602
|
+
// at most one round trip per *distinct* ACL uid (typically just the shared parent, since per-record
|
|
603
|
+
// ACLs are rarely warm in Redis) instead of N sequential round trips.
|
|
604
|
+
//
|
|
605
|
+
// A soft-deleted row can appear in `results` despite `buildSearchQuery()`'s default exclusion via a client
|
|
606
|
+
// supplying its own `deleted` query param, which the query builder honors as-is. Such a row needs the
|
|
607
|
+
// same DELETE+UPDATE permission.
|
|
608
|
+
if (this.aclUtils?.enabled && !options?.ignoreACL) {
|
|
609
|
+
const recordACL = !!this.modelClass.recordACL;
|
|
610
|
+
const permitted = await Promise.all(results.map((obj) => {
|
|
611
|
+
if (obj.deleted === true) {
|
|
612
|
+
return this.canViewDeleted(options?.user, recordACL ? obj.uid : this.defaultACLUid);
|
|
613
|
+
}
|
|
614
|
+
return recordACL
|
|
615
|
+
? this.aclUtils.hasPermission(options?.user, obj.uid, action)
|
|
616
|
+
: Promise.resolve(true);
|
|
617
|
+
}));
|
|
618
|
+
results = results.filter((_obj, i) => permitted[i]);
|
|
619
|
+
}
|
|
620
|
+
// Process the results to remove any properties that have been scoped with @RequiresScope that the user
|
|
621
|
+
// does not have access to.
|
|
622
|
+
ObjectUtils.deleteScopedProps(results, options?.user, this.modelClass);
|
|
382
623
|
return results;
|
|
383
624
|
}
|
|
384
625
|
/**
|
|
@@ -392,39 +633,78 @@ export class RepoUtils {
|
|
|
392
633
|
if (!this.repo) {
|
|
393
634
|
throw new ApiError(ApiErrors.INTERNAL_ERROR, 500, ApiErrorMessages.INTERNAL_ERROR);
|
|
394
635
|
}
|
|
636
|
+
let existing = undefined;
|
|
637
|
+
const txInfo = this.getTransaction(options);
|
|
638
|
+
// Deliberately uses the default (includeDeleted: true) query shape here — this result is cached under
|
|
639
|
+
// a key shared with create()/update()/find()'s cache-seeding, all of which also use the default shape,
|
|
640
|
+
// so changing it here alone would desync this read from what those write. Soft-deleted records are
|
|
641
|
+
// filtered out below instead, after the cache/DB read, regardless of which one produced the result.
|
|
395
642
|
const query = this.searchIdQuery(id, options?.version);
|
|
396
|
-
if (!options?.skipCache && this.
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
643
|
+
if (!options?.skipCache && this.cache) {
|
|
644
|
+
existing = await this.cache.load(this.hashQuery(query));
|
|
645
|
+
}
|
|
646
|
+
if (!existing) {
|
|
647
|
+
if (this.repo instanceof MongoRepository) {
|
|
648
|
+
existing = await this.repo
|
|
649
|
+
.find(query["$match"] ? query["$match"] : query, {
|
|
650
|
+
session: txInfo?.session,
|
|
651
|
+
sort: { version: -1 },
|
|
652
|
+
})
|
|
653
|
+
.next();
|
|
654
|
+
}
|
|
655
|
+
else if (this.modelClass.prototype instanceof BaseEntity) {
|
|
656
|
+
// Without an explicit version, `query` matches every row sharing this uid (all historical
|
|
657
|
+
// versions, for a trackChanges entity). Order by version desc so the newest one wins, the same
|
|
658
|
+
// way the Mongo branch above does - otherwise TypeORM's `findOne()` returns whichever matching
|
|
659
|
+
// row it encounters first, which isn't guaranteed to be the latest. `SimpleEntity` has no
|
|
660
|
+
// `version` column to order by, so this only applies to `BaseEntity` subclasses.
|
|
661
|
+
const repo = txInfo?.entityManager ? txInfo.entityManager.getRepository(this.modelClass) : this.repo;
|
|
662
|
+
existing = (await repo.findOne({ ...query, order: { version: "DESC" } }));
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
const repo = txInfo?.entityManager ? txInfo.entityManager.getRepository(this.modelClass) : this.repo;
|
|
666
|
+
existing = (await repo.findOne(query));
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
// Never surface a soft-deleted record via an id-based lookup by default — matches the default the
|
|
670
|
+
// list/search endpoint already applies. `includeDeleted` opts back in (e.g. an admin history/restore
|
|
671
|
+
// view fetching a specific past version by id). Checked here (after cache or DB resolution) rather
|
|
672
|
+
// than by filtering `deleted` into the query above, so a cache entry that predates a delete, or was
|
|
673
|
+
// seeded by an explicit `?deleted=true` list request, is filtered consistently either way.
|
|
674
|
+
if (existing && existing.deleted === true && !options?.includeDeleted) {
|
|
675
|
+
existing = null;
|
|
676
|
+
}
|
|
677
|
+
if (existing) {
|
|
678
|
+
if (this.cache) {
|
|
679
|
+
// Cache the object for faster retrieval
|
|
680
|
+
this.cache.save(this.hashQuery(query), existing).catch((err) => this.logCacheError("save", err));
|
|
681
|
+
}
|
|
682
|
+
// Check user permissions
|
|
683
|
+
if (this.aclUtils?.enabled && !options?.ignoreACL) {
|
|
684
|
+
const acl = await this.aclUtils.findACL(existing.uid);
|
|
685
|
+
if (existing.deleted === true) {
|
|
686
|
+
// Viewing a soft-deleted record (opted into via `includeDeleted`) requires both DELETE and UPDATE
|
|
687
|
+
// permission, the two actions actually needed to restore the record.
|
|
688
|
+
if (!(await this.canViewDeleted(options?.user, acl ? acl : this.defaultACLUid))) {
|
|
689
|
+
existing = null;
|
|
404
690
|
}
|
|
405
691
|
}
|
|
406
|
-
|
|
407
|
-
|
|
692
|
+
else {
|
|
693
|
+
const action = options?.action ?? ACLAction.READ;
|
|
694
|
+
if (!(await this.aclUtils.hasPermission(options?.user, acl ? acl : this.defaultACLUid, action))) {
|
|
695
|
+
throw new ApiError(ApiErrors.AUTH_PERMISSION_FAILURE, 403, ApiErrorMessages.AUTH_PERMISSION_FAILURE);
|
|
696
|
+
}
|
|
408
697
|
}
|
|
409
698
|
}
|
|
410
699
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
})
|
|
417
|
-
.next();
|
|
418
|
-
}
|
|
419
|
-
else {
|
|
420
|
-
existing = await this.repo.findOne(query);
|
|
421
|
-
}
|
|
422
|
-
if (existing && this.cacheClient && this.modelClass.cacheTTL) {
|
|
423
|
-
// Cache the object for faster retrieval
|
|
424
|
-
void this.cacheClient.setex(`${this.baseCacheKey}.${this.hashQuery(query)}`, this.modelClass.cacheTTL, JSON.stringify(existing));
|
|
700
|
+
const result = existing ? this.instantiateObject(existing) : undefined;
|
|
701
|
+
// Process the result to remove any properties that have been scoped with @RequiresScope that the user
|
|
702
|
+
// does not have access to.
|
|
703
|
+
if (result) {
|
|
704
|
+
ObjectUtils.deleteScopedProps(result, options?.user, this.modelClass);
|
|
425
705
|
}
|
|
426
706
|
// Make sure we return the correct data type
|
|
427
|
-
return
|
|
707
|
+
return result;
|
|
428
708
|
}
|
|
429
709
|
/**
|
|
430
710
|
* Returns the default access control list governing the model type. Returning a value of `undefined` will grant
|
|
@@ -442,13 +722,47 @@ export class RepoUtils {
|
|
|
442
722
|
}
|
|
443
723
|
return result;
|
|
444
724
|
}
|
|
725
|
+
/**
|
|
726
|
+
* Checks whether the caller has both `DELETE` and `UPDATE` permission against the given ACL (or ACL uid).
|
|
727
|
+
* Ordinary READ/LIST/EXISTS permission on a record says nothing about whether its owner
|
|
728
|
+
* consented to its "deleted" state being visible, so those two actions (the ones actually needed to
|
|
729
|
+
* restore the record) are required instead.
|
|
730
|
+
*
|
|
731
|
+
* @param user The user to check.
|
|
732
|
+
* @param acl The ACL (or ACL uid) governing the record.
|
|
733
|
+
*/
|
|
734
|
+
async canViewDeleted(user, acl) {
|
|
735
|
+
return ((await this.aclUtils.hasPermission(user, acl, ACLAction.DELETE)) &&
|
|
736
|
+
(await this.aclUtils.hasPermission(user, acl, ACLAction.UPDATE)));
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Logs a swallowed error from a fire-and-forget cache operation. Cache reads/writes are best-effort and
|
|
740
|
+
* must never propagate into (and thus fail/retry) the write they're attached to.
|
|
741
|
+
* @param op The cache operation that failed (e.g. "save", "delete").
|
|
742
|
+
* @param err The error thrown by the cache operation.
|
|
743
|
+
*/
|
|
744
|
+
logCacheError(op, err) {
|
|
745
|
+
this.logger?.warn(`RepoUtils: Cache ${op} failed for ${this.modelClass?.name}.`);
|
|
746
|
+
this.logger?.debug(err);
|
|
747
|
+
}
|
|
445
748
|
/**
|
|
446
749
|
* Hashes the given query object to a unique string.
|
|
447
750
|
* @param query The query object to hash.
|
|
448
751
|
*/
|
|
449
752
|
hashQuery(query) {
|
|
450
753
|
const queryStr = JSON.stringify(query);
|
|
451
|
-
|
|
754
|
+
let hash = _hashCache.get(queryStr);
|
|
755
|
+
if (hash === undefined) {
|
|
756
|
+
// Hash the query string
|
|
757
|
+
hash = crypto.createHash("md5").update(queryStr).digest("hex");
|
|
758
|
+
// Clear the hash cache if it grows too big to prevent runaway memory usage
|
|
759
|
+
if (_hashCache.size >= 10000) {
|
|
760
|
+
_hashCache.clear();
|
|
761
|
+
}
|
|
762
|
+
// Store the hashed query string for faster lookup next time
|
|
763
|
+
_hashCache.set(queryStr, hash);
|
|
764
|
+
}
|
|
765
|
+
return hash;
|
|
452
766
|
}
|
|
453
767
|
/**
|
|
454
768
|
* Returns the class type (constructor) for the given object. This uses the `_fqn` or `_type` property of `obj` to
|
|
@@ -459,14 +773,28 @@ export class RepoUtils {
|
|
|
459
773
|
*/
|
|
460
774
|
getClassType(obj) {
|
|
461
775
|
const className = obj._fqn || obj._type;
|
|
462
|
-
if (this.
|
|
776
|
+
if (this._objectFactory) {
|
|
463
777
|
if (className && typeof className === "string") {
|
|
464
|
-
const clazz = this.
|
|
465
|
-
|
|
778
|
+
const clazz = this._objectFactory.classes.get(className) ||
|
|
779
|
+
this._objectFactory.classes.get(`models.${className}`);
|
|
780
|
+
// Only accept the resolved class if it's actually this route's model or a subtype of it (e.g. a
|
|
781
|
+
// @ChildEntity()). `objectFactory.classes` contains every registered model in the app, so without
|
|
782
|
+
// this check a client could point `_type`/`_fqn` at an unrelated model to have its payload
|
|
783
|
+
// instantiated/validated against that other model's (possibly much looser) rules while still
|
|
784
|
+
// being persisted through this route's own datasource/collection.
|
|
785
|
+
if (clazz && (clazz === this.modelClass || clazz.prototype instanceof this.modelClass)) {
|
|
786
|
+
return clazz;
|
|
787
|
+
}
|
|
466
788
|
}
|
|
467
789
|
}
|
|
468
790
|
return this.modelClass;
|
|
469
791
|
}
|
|
792
|
+
/**
|
|
793
|
+
* Returns the current transactional session information, if present.
|
|
794
|
+
*/
|
|
795
|
+
getTransaction(options) {
|
|
796
|
+
return options?.transaction ?? transactionContext.getStore();
|
|
797
|
+
}
|
|
470
798
|
/**
|
|
471
799
|
* Creates a new instance of obj scoped to the correct model class or sub-class.
|
|
472
800
|
*/
|
|
@@ -480,55 +808,81 @@ export class RepoUtils {
|
|
|
480
808
|
* Search for existing object based on passed in id and version and product uid.
|
|
481
809
|
*
|
|
482
810
|
* The result of this function is compatible with all `Repository.find()` functions.
|
|
811
|
+
*
|
|
812
|
+
* @param includeDeleted Set to false to exclude soft-deleted `RecoverableBaseEntity` records from matching.
|
|
813
|
+
* Defaults to true; pass false when the result is exposed directly to an API client (e.g. `findOne`, `exists`)
|
|
814
|
+
* so a soft-deleted record isn't returned as if it still existed.
|
|
483
815
|
*/
|
|
484
|
-
searchIdQuery(id, version) {
|
|
485
|
-
return ModelUtils.buildIdSearchQuery(this.repo, this.modelClass, id, typeof version === "string" ? parseInt(version, 10) : version);
|
|
816
|
+
searchIdQuery(id, version, includeDeleted = true) {
|
|
817
|
+
return ModelUtils.buildIdSearchQuery(this.repo, this.modelClass, id, typeof version === "string" ? parseInt(version, 10) : version, includeDeleted);
|
|
486
818
|
}
|
|
487
819
|
async truncate(query, options) {
|
|
488
820
|
if (!this.repo) {
|
|
489
821
|
throw new ApiError(ApiErrors.INTERNAL_ERROR, 500, ApiErrorMessages.INTERNAL_ERROR);
|
|
490
822
|
}
|
|
823
|
+
const txInfo = this.getTransaction(options);
|
|
491
824
|
// Check user permissions. Don't check if record-level ACLs are used as this will be done
|
|
492
825
|
// per record later.
|
|
493
826
|
if (this.aclUtils?.enabled && !options.ignoreACL && !this.modelClass.recordACL) {
|
|
494
|
-
if (!(await this.aclUtils.hasPermission(options.user, this.defaultACLUid, ACLAction.
|
|
827
|
+
if (!(await this.aclUtils.hasPermission(options.user, this.defaultACLUid, ACLAction.TRUNCATE))) {
|
|
495
828
|
throw new ApiError(ApiErrors.AUTH_PERMISSION_FAILURE, 403, ApiErrorMessages.AUTH_PERMISSION_FAILURE);
|
|
496
829
|
}
|
|
497
830
|
}
|
|
498
831
|
try {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
if (Array.isArray(query)) {
|
|
502
|
-
uids = await this.repo.distinct("uid", query[0].$match);
|
|
503
|
-
}
|
|
504
|
-
else {
|
|
505
|
-
uids = await this.repo.distinct("uid", query["$match"] ? query["$match"] : query);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
else {
|
|
509
|
-
(await this.repo.find(query)).forEach((obj) => uids.push(obj.uid));
|
|
510
|
-
}
|
|
832
|
+
const searchQuery = ModelUtils.buildSearchQuery(this.modelClass, this.repo, query, true, options?.user);
|
|
833
|
+
const uids = await this.findAllUids(searchQuery, options);
|
|
511
834
|
if (uids.length > 0) {
|
|
512
835
|
let finalUids = uids;
|
|
513
836
|
// Check if this class uses record level ACLs. If so, we need to check the perms of
|
|
514
837
|
// each one. We will remove any from our list that the user does not have permission to
|
|
515
|
-
//
|
|
838
|
+
// truncate.
|
|
516
839
|
if (this.aclUtils?.enabled && this.modelClass.recordACL) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
840
|
+
if (options.ignoreACL) {
|
|
841
|
+
// Caller has already authorized this truncate; skip the per-record permission
|
|
842
|
+
// narrowing entirely rather than defaulting to an empty (i.e. no-op) delete set.
|
|
843
|
+
finalUids = uids;
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
finalUids = await this.filterPermittedUids(uids, ACLAction.TRUNCATE, options);
|
|
524
847
|
}
|
|
525
848
|
}
|
|
849
|
+
const cleansUpRecordACLs = !!(this.aclUtils?.enabled && this.modelClass.recordACL);
|
|
526
850
|
// Now delete all records that were found
|
|
527
851
|
if (this.repo instanceof MongoRepository) {
|
|
528
|
-
await this.repo.deleteMany({ uid: { $in: finalUids } }
|
|
852
|
+
await this.repo.deleteMany({ uid: { $in: finalUids } }, {
|
|
853
|
+
session: txInfo?.session,
|
|
854
|
+
});
|
|
529
855
|
}
|
|
530
856
|
else {
|
|
531
|
-
|
|
857
|
+
const repo = txInfo?.entityManager
|
|
858
|
+
? txInfo.entityManager.getRepository(this.modelClass)
|
|
859
|
+
: this.repo;
|
|
860
|
+
// A plain array of ids only maps to a WHERE ... IN clause when the primary key is a single
|
|
861
|
+
// column — for a trackChanges entity the SQL primary key is the composite (uid, version),
|
|
862
|
+
// so an explicit In() on the uid column is used instead of relying on that implicit form.
|
|
863
|
+
const { In } = ModelUtils.orm;
|
|
864
|
+
await repo.delete({ uid: In(finalUids) });
|
|
865
|
+
}
|
|
866
|
+
if (cleansUpRecordACLs && finalUids.length > 0) {
|
|
867
|
+
// `removeACLs()` returns exactly what it deleted (captured atomically, not via a separate
|
|
868
|
+
// earlier read - see `removeACL()`'s doc comment) - used directly as the restore snapshot.
|
|
869
|
+
// It commits independently, on the `acl` connection's own transaction; if this (entity-side)
|
|
870
|
+
// transaction later fails, its own abort can't undo that removal, so the rollback hook
|
|
871
|
+
// restores the snapshot in that case. `saveACLs()` restores each ACL's exact prior version
|
|
872
|
+
// (see `saveACL()`'s `preserveVersion` option) and refuses — rather than clobbers — any of
|
|
873
|
+
// these uids that already has something at it again by the time the restore runs.
|
|
874
|
+
const removedAcls = await this.aclUtils.removeACLs(finalUids);
|
|
875
|
+
if (removedAcls.length > 0) {
|
|
876
|
+
registerRollbackHook(async () => {
|
|
877
|
+
try {
|
|
878
|
+
await this.aclUtils.saveACLs(removedAcls);
|
|
879
|
+
}
|
|
880
|
+
catch (err) {
|
|
881
|
+
this.logger?.warn(`RepoUtils: Failed to restore ${removedAcls.length} ACL(s) after a failed truncate().`);
|
|
882
|
+
this.logger?.debug(err);
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
}
|
|
532
886
|
}
|
|
533
887
|
if (!options?.skipPush) {
|
|
534
888
|
let channels = options?.pushChannels || [];
|
|
@@ -553,6 +907,7 @@ export class RepoUtils {
|
|
|
553
907
|
if (!this.repo) {
|
|
554
908
|
throw new ApiError(ApiErrors.INTERNAL_ERROR, 500, ApiErrorMessages.INTERNAL_ERROR);
|
|
555
909
|
}
|
|
910
|
+
const txInfo = this.getTransaction(options);
|
|
556
911
|
if (this.aclUtils?.enabled && !options?.ignoreACL) {
|
|
557
912
|
const acl = await this.aclUtils.findACL(existing.uid);
|
|
558
913
|
if (!(await this.aclUtils.hasPermission(options?.user, acl ? acl : this.defaultACLUid, ACLAction.UPDATE))) {
|
|
@@ -569,6 +924,15 @@ export class RepoUtils {
|
|
|
569
924
|
if (existing.uid !== obj.uid) {
|
|
570
925
|
throw new ApiError(ApiErrors.OBJECT_ID_MISMATCH, 400, ApiErrorMessages.OBJECT_ID_MISMATCH);
|
|
571
926
|
}
|
|
927
|
+
// Force system-managed fields back to their persisted value, discarding whatever the client sent (or
|
|
928
|
+
// didn't send) for them. `dateCreated` is always protected; `@ReadOnly`-decorated properties are an
|
|
929
|
+
// app-level opt-in for anything else (roles, ownership fields, etc.) that must never be client-settable.
|
|
930
|
+
if (existing instanceof BaseEntity) {
|
|
931
|
+
obj.dateCreated = existing.dateCreated;
|
|
932
|
+
}
|
|
933
|
+
for (const prop of ModelUtils.getReadOnlyPropertyNames(this.modelClass)) {
|
|
934
|
+
obj[prop] = existing[prop];
|
|
935
|
+
}
|
|
572
936
|
// When using MongoDB we need to copy the _id property in order to prevent duplicate entries
|
|
573
937
|
if (existing instanceof BaseMongoEntity) {
|
|
574
938
|
obj._id = existing._id;
|
|
@@ -579,12 +943,24 @@ export class RepoUtils {
|
|
|
579
943
|
if (this.repo instanceof MongoRepository) {
|
|
580
944
|
if (existing instanceof BaseEntity) {
|
|
581
945
|
if (keepPrevious) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
946
|
+
// Same (uid, version) unique index race as RepoUtils.create(): two concurrent updates of
|
|
947
|
+
// the same version can both pass the optimistic-lock check above and both attempt to
|
|
948
|
+
// insert (uid, version + 1). The database rejects the loser; treat that the same as a
|
|
949
|
+
// lost optimistic-lock race rather than letting the raw duplicate-key error escape.
|
|
950
|
+
try {
|
|
951
|
+
result = this.instantiateObject(await this.repo.save({
|
|
952
|
+
...obj,
|
|
953
|
+
_id: undefined, // Ensure we save a new document
|
|
954
|
+
dateModified: new Date(),
|
|
955
|
+
version: obj.version + 1,
|
|
956
|
+
}, { session: txInfo?.session }));
|
|
957
|
+
}
|
|
958
|
+
catch (err) {
|
|
959
|
+
if (err?.code === 11000) {
|
|
960
|
+
throw new ApiError(ApiErrors.INVALID_OBJECT_VERSION, 409, ApiErrorMessages.INVALID_OBJECT_VERSION);
|
|
961
|
+
}
|
|
962
|
+
throw err;
|
|
963
|
+
}
|
|
588
964
|
}
|
|
589
965
|
else {
|
|
590
966
|
await this.repo.updateOne({ uid: obj.uid, version: obj.version }, {
|
|
@@ -593,22 +969,32 @@ export class RepoUtils {
|
|
|
593
969
|
dateModified: new Date(),
|
|
594
970
|
version: obj.version + 1,
|
|
595
971
|
},
|
|
972
|
+
}, {
|
|
973
|
+
session: txInfo?.session,
|
|
596
974
|
});
|
|
597
975
|
}
|
|
598
976
|
}
|
|
599
977
|
else if (obj.uid) {
|
|
600
978
|
if (keepPrevious) {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
979
|
+
try {
|
|
980
|
+
result = this.instantiateObject(await this.repo.save({
|
|
981
|
+
...obj,
|
|
982
|
+
version: obj.version + 1,
|
|
983
|
+
}, { session: txInfo?.session }));
|
|
984
|
+
}
|
|
985
|
+
catch (err) {
|
|
986
|
+
if (err?.code === 11000) {
|
|
987
|
+
throw new ApiError(ApiErrors.INVALID_OBJECT_VERSION, 409, ApiErrorMessages.INVALID_OBJECT_VERSION);
|
|
988
|
+
}
|
|
989
|
+
throw err;
|
|
990
|
+
}
|
|
605
991
|
}
|
|
606
992
|
else {
|
|
607
993
|
await this.repo.updateOne({ uid: obj.uid }, {
|
|
608
994
|
$set: {
|
|
609
995
|
...obj,
|
|
610
996
|
},
|
|
611
|
-
});
|
|
997
|
+
}, { session: txInfo?.session });
|
|
612
998
|
}
|
|
613
999
|
}
|
|
614
1000
|
else {
|
|
@@ -616,20 +1002,21 @@ export class RepoUtils {
|
|
|
616
1002
|
if (keepPrevious) {
|
|
617
1003
|
toSave.version += 1;
|
|
618
1004
|
}
|
|
619
|
-
result = await this.repo.save(toSave);
|
|
1005
|
+
result = await this.repo.save(toSave, { session: txInfo?.session });
|
|
620
1006
|
}
|
|
621
1007
|
}
|
|
622
1008
|
else {
|
|
1009
|
+
const repo = txInfo?.entityManager ? txInfo.entityManager.getRepository(this.modelClass) : this.repo;
|
|
623
1010
|
if (existing instanceof BaseEntity) {
|
|
624
1011
|
if (keepPrevious) {
|
|
625
|
-
await
|
|
1012
|
+
await repo.insert({
|
|
626
1013
|
...obj,
|
|
627
1014
|
dateModified: new Date(),
|
|
628
1015
|
version: obj.version + 1,
|
|
629
1016
|
});
|
|
630
1017
|
}
|
|
631
1018
|
else {
|
|
632
|
-
await
|
|
1019
|
+
await repo.update(query.where, {
|
|
633
1020
|
...obj,
|
|
634
1021
|
dateModified: new Date(),
|
|
635
1022
|
version: obj.version + 1,
|
|
@@ -640,31 +1027,43 @@ export class RepoUtils {
|
|
|
640
1027
|
const toSave = obj;
|
|
641
1028
|
if (keepPrevious) {
|
|
642
1029
|
toSave.version += 1;
|
|
643
|
-
|
|
1030
|
+
// TypeORM's overloaded Repository.save() can't be resolved against `repo`'s inferred
|
|
1031
|
+
// `EntityManager | Repository<T>` union type — same class of friction as the "HAX" cast
|
|
1032
|
+
// above.
|
|
1033
|
+
result = await repo.save(toSave);
|
|
644
1034
|
}
|
|
645
1035
|
else {
|
|
646
|
-
await
|
|
1036
|
+
await repo.update(query.where, toSave);
|
|
647
1037
|
}
|
|
648
1038
|
}
|
|
649
1039
|
}
|
|
650
|
-
query = this.searchIdQuery(existing.uid,
|
|
1040
|
+
query = this.searchIdQuery(existing.uid, existing instanceof BaseEntity ? existing.version + 1 : undefined);
|
|
651
1041
|
if (!result) {
|
|
652
1042
|
if (this.repo instanceof MongoRepository) {
|
|
653
|
-
result = await this.repo.findOne(query["$match"] ? query["$match"] : query
|
|
1043
|
+
result = await this.repo.findOne(query["$match"] ? query["$match"] : query, {
|
|
1044
|
+
session: txInfo?.session,
|
|
1045
|
+
});
|
|
654
1046
|
}
|
|
655
1047
|
else {
|
|
656
|
-
|
|
1048
|
+
const repo = txInfo?.entityManager ? txInfo.entityManager.getRepository(this.modelClass) : this.repo;
|
|
1049
|
+
result = (await repo.findOne(query));
|
|
657
1050
|
}
|
|
658
1051
|
if (!result) {
|
|
659
1052
|
throw new ApiError(ApiErrors.INTERNAL_ERROR, 500, ApiErrorMessages.INTERNAL_ERROR);
|
|
660
1053
|
}
|
|
661
1054
|
}
|
|
662
1055
|
result = this.instantiateObject(result);
|
|
663
|
-
if (result && this.
|
|
664
|
-
// Cache the object for faster retrieval
|
|
665
|
-
|
|
666
|
-
|
|
1056
|
+
if (result && this.cache) {
|
|
1057
|
+
// Cache the object for faster retrieval.
|
|
1058
|
+
this.cache.save(this.hashQuery(query), result).catch((err) => this.logCacheError("save", err));
|
|
1059
|
+
this.cache
|
|
1060
|
+
.save(this.hashQuery(this.searchIdQuery(result.uid)), result)
|
|
1061
|
+
.catch((err) => this.logCacheError("save", err));
|
|
667
1062
|
}
|
|
1063
|
+
// Process the result to remove any properties that have been scoped with @RequiresScope that the user
|
|
1064
|
+
// does not have access to. Done after caching (the cache must retain the full object for other
|
|
1065
|
+
// requests) but before the result is returned or broadcast to push subscribers.
|
|
1066
|
+
ObjectUtils.deleteScopedProps(result, options?.user, this.modelClass);
|
|
668
1067
|
if (!options?.skipPush) {
|
|
669
1068
|
let channels = [result.uid].concat(options?.pushChannels || []);
|
|
670
1069
|
this.notificationUtils?.sendMessage(channels, this.modelClass.name, "update", result);
|
|
@@ -686,18 +1085,31 @@ export class RepoUtils {
|
|
|
686
1085
|
// Instantiate the correct object type so that we can perform validation correctly. If we don't do this
|
|
687
1086
|
// then the provided object will be missing all decorators and validation won't work as desired.
|
|
688
1087
|
const metadataObj = this.instantiateObject(obj);
|
|
1088
|
+
// A separate, genuinely bare instance (constructed with no data at all) to source @ReadOnly
|
|
1089
|
+
// defaults from. `metadataObj` isn't safe for this: it's hydrated from the client-supplied
|
|
1090
|
+
// `obj`, and a model constructor that copies a same-named field from its `other` argument
|
|
1091
|
+
// (a common "hydrate from data" pattern) would carry the client's tampered value straight
|
|
1092
|
+
// through to `metadataObj` too, making the "reset to default" below a no-op.
|
|
1093
|
+
const defaultObj = this.instantiateObject(undefined, metadataObj.constructor);
|
|
689
1094
|
ObjectUtils.validate(obj, metadataObj.constructor);
|
|
690
|
-
// Iterate through all properties
|
|
1095
|
+
// Iterate through all properties
|
|
691
1096
|
for (const member of Object.getOwnPropertyNames(obj)) {
|
|
1097
|
+
// Reset any @ReadOnly properties, discarding whatever the client supplied.
|
|
1098
|
+
const isReadOnly = Reflect.getMetadata("rrst:readOnly", metadataObj, member);
|
|
1099
|
+
if (member in obj && isReadOnly) {
|
|
1100
|
+
// Override the value from our default object
|
|
1101
|
+
obj[member] = defaultObj[member];
|
|
1102
|
+
}
|
|
1103
|
+
// Check for @Reference
|
|
692
1104
|
const clazz = Reflect.getMetadata("rrst:reference", metadataObj, member);
|
|
693
|
-
if (clazz && clazz.
|
|
1105
|
+
if (clazz && clazz.datasource && obj[member]) {
|
|
694
1106
|
// Attempt to grab the repository for this reference type
|
|
695
|
-
const conn = this.connectionManager?.connections.get(clazz.
|
|
1107
|
+
const conn = this.connectionManager?.connections.get(clazz.datasource);
|
|
696
1108
|
const repo = conn instanceof MongoConnection || isSqlDataSource(conn)
|
|
697
1109
|
? conn.getRepository(clazz)
|
|
698
1110
|
: undefined;
|
|
699
1111
|
if (repo) {
|
|
700
|
-
// Check to see if there are any objects with this UID in the
|
|
1112
|
+
// Check to see if there are any objects with this UID in the datasource. If the value is an array
|
|
701
1113
|
// let's make sure that every uid is valid.
|
|
702
1114
|
const uids = Array.isArray(obj[member]) ? obj[member] : [obj[member]];
|
|
703
1115
|
const query = ModelUtils.buildIdSearchQuery(repo, clazz, uids);
|
|
@@ -711,7 +1123,7 @@ export class RepoUtils {
|
|
|
711
1123
|
}
|
|
712
1124
|
}
|
|
713
1125
|
catch (err) {
|
|
714
|
-
throw new ApiError(
|
|
1126
|
+
throw new ApiError(ApiErrors.INVALID_REQUEST, 400, err.message);
|
|
715
1127
|
}
|
|
716
1128
|
}
|
|
717
1129
|
}
|
|
@@ -719,10 +1131,6 @@ __decorate([
|
|
|
719
1131
|
Inject("ACLUtils"),
|
|
720
1132
|
__metadata("design:type", Function)
|
|
721
1133
|
], RepoUtils.prototype, "aclUtils", void 0);
|
|
722
|
-
__decorate([
|
|
723
|
-
RedisConnection("cache"),
|
|
724
|
-
__metadata("design:type", Redis)
|
|
725
|
-
], RepoUtils.prototype, "cacheClient", void 0);
|
|
726
1134
|
__decorate([
|
|
727
1135
|
Config(),
|
|
728
1136
|
__metadata("design:type", Object)
|
|
@@ -735,10 +1143,6 @@ __decorate([
|
|
|
735
1143
|
Logger,
|
|
736
1144
|
__metadata("design:type", Object)
|
|
737
1145
|
], RepoUtils.prototype, "logger", void 0);
|
|
738
|
-
__decorate([
|
|
739
|
-
Inject(ObjectFactory),
|
|
740
|
-
__metadata("design:type", ObjectFactory)
|
|
741
|
-
], RepoUtils.prototype, "objectFactory", void 0);
|
|
742
1146
|
__decorate([
|
|
743
1147
|
Inject(NotificationUtils),
|
|
744
1148
|
__metadata("design:type", NotificationUtils)
|
|
@@ -753,4 +1157,28 @@ __decorate([
|
|
|
753
1157
|
__metadata("design:paramtypes", []),
|
|
754
1158
|
__metadata("design:returntype", Promise)
|
|
755
1159
|
], RepoUtils.prototype, "init", null);
|
|
1160
|
+
__decorate([
|
|
1161
|
+
Transactional(),
|
|
1162
|
+
__metadata("design:type", Function),
|
|
1163
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
1164
|
+
__metadata("design:returntype", Promise)
|
|
1165
|
+
], RepoUtils.prototype, "create", null);
|
|
1166
|
+
__decorate([
|
|
1167
|
+
Transactional(),
|
|
1168
|
+
__metadata("design:type", Function),
|
|
1169
|
+
__metadata("design:paramtypes", [String, Object]),
|
|
1170
|
+
__metadata("design:returntype", Promise)
|
|
1171
|
+
], RepoUtils.prototype, "delete", null);
|
|
1172
|
+
__decorate([
|
|
1173
|
+
Transactional(),
|
|
1174
|
+
__metadata("design:type", Function),
|
|
1175
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
1176
|
+
__metadata("design:returntype", Promise)
|
|
1177
|
+
], RepoUtils.prototype, "truncate", null);
|
|
1178
|
+
__decorate([
|
|
1179
|
+
Transactional(),
|
|
1180
|
+
__metadata("design:type", Function),
|
|
1181
|
+
__metadata("design:paramtypes", [Object, Object, Object]),
|
|
1182
|
+
__metadata("design:returntype", Promise)
|
|
1183
|
+
], RepoUtils.prototype, "update", null);
|
|
756
1184
|
//# sourceMappingURL=RepoUtils.js.map
|