@rapidrest/service-core 1.0.0-rc.7 → 1.0.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 -203
- 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,88 +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(cresults[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
|
-
// Populate the cache with all missing objects
|
|
335
|
-
for (const obj of missing) {
|
|
336
|
-
const query = this.searchIdQuery(obj.uid);
|
|
337
|
-
const cacheKey = `${this.baseCacheKey}.${this.hashQuery(query)}`;
|
|
338
|
-
void this.cacheClient.setex(cacheKey, this.modelClass.cacheTTL, JSON.stringify(obj));
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
catch (err) {
|
|
342
|
-
// It doesn't matter if this fails
|
|
343
|
-
}
|
|
555
|
+
if (!options?.skipCache && this.cache) {
|
|
556
|
+
const cached = await this.cache.loadSet(searchQueryHash);
|
|
557
|
+
if (cached) {
|
|
558
|
+
results = cached.filter((obj) => obj !== undefined);
|
|
344
559
|
}
|
|
345
560
|
}
|
|
346
561
|
// If the query wasn't cached retrieve from the database
|
|
347
562
|
if (results.length === 0) {
|
|
563
|
+
const searchQuery = ModelUtils.buildSearchQuery(this.modelClass, this.repo, query, true, options?.user);
|
|
348
564
|
if (this.repo instanceof MongoRepository) {
|
|
349
565
|
const skip = page * limit;
|
|
350
|
-
if (Array.isArray(
|
|
351
|
-
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();
|
|
352
572
|
}
|
|
353
573
|
else {
|
|
354
574
|
results = await this.repo
|
|
355
|
-
.find(
|
|
575
|
+
.find(searchQuery["$match"] ? searchQuery["$match"] : searchQuery, {
|
|
356
576
|
limit,
|
|
577
|
+
session: txInfo?.session,
|
|
357
578
|
skip,
|
|
358
|
-
sort:
|
|
579
|
+
sort: searchQuery["$sort"],
|
|
359
580
|
})
|
|
360
581
|
.toArray();
|
|
361
582
|
}
|
|
362
583
|
}
|
|
363
584
|
else {
|
|
364
|
-
|
|
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 }));
|
|
365
590
|
}
|
|
366
591
|
// Cache the results for future requests. Don't bother if there were no results.
|
|
367
|
-
if (results.length > 0 && this.
|
|
368
|
-
|
|
369
|
-
|
|
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));
|
|
370
597
|
}
|
|
371
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);
|
|
372
623
|
return results;
|
|
373
624
|
}
|
|
374
625
|
/**
|
|
@@ -382,39 +633,78 @@ export class RepoUtils {
|
|
|
382
633
|
if (!this.repo) {
|
|
383
634
|
throw new ApiError(ApiErrors.INTERNAL_ERROR, 500, ApiErrorMessages.INTERNAL_ERROR);
|
|
384
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.
|
|
385
642
|
const query = this.searchIdQuery(id, options?.version);
|
|
386
|
-
if (!options?.skipCache && this.
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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;
|
|
394
690
|
}
|
|
395
691
|
}
|
|
396
|
-
|
|
397
|
-
|
|
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
|
+
}
|
|
398
697
|
}
|
|
399
698
|
}
|
|
400
699
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
})
|
|
407
|
-
.next();
|
|
408
|
-
}
|
|
409
|
-
else {
|
|
410
|
-
existing = await this.repo.findOne(query);
|
|
411
|
-
}
|
|
412
|
-
if (existing && this.cacheClient && this.modelClass.cacheTTL) {
|
|
413
|
-
// Cache the object for faster retrieval
|
|
414
|
-
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);
|
|
415
705
|
}
|
|
416
706
|
// Make sure we return the correct data type
|
|
417
|
-
return
|
|
707
|
+
return result;
|
|
418
708
|
}
|
|
419
709
|
/**
|
|
420
710
|
* Returns the default access control list governing the model type. Returning a value of `undefined` will grant
|
|
@@ -432,13 +722,47 @@ export class RepoUtils {
|
|
|
432
722
|
}
|
|
433
723
|
return result;
|
|
434
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
|
+
}
|
|
435
748
|
/**
|
|
436
749
|
* Hashes the given query object to a unique string.
|
|
437
750
|
* @param query The query object to hash.
|
|
438
751
|
*/
|
|
439
752
|
hashQuery(query) {
|
|
440
753
|
const queryStr = JSON.stringify(query);
|
|
441
|
-
|
|
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;
|
|
442
766
|
}
|
|
443
767
|
/**
|
|
444
768
|
* Returns the class type (constructor) for the given object. This uses the `_fqn` or `_type` property of `obj` to
|
|
@@ -449,14 +773,28 @@ export class RepoUtils {
|
|
|
449
773
|
*/
|
|
450
774
|
getClassType(obj) {
|
|
451
775
|
const className = obj._fqn || obj._type;
|
|
452
|
-
if (this.
|
|
776
|
+
if (this._objectFactory) {
|
|
453
777
|
if (className && typeof className === "string") {
|
|
454
|
-
const clazz = this.
|
|
455
|
-
|
|
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
|
+
}
|
|
456
788
|
}
|
|
457
789
|
}
|
|
458
790
|
return this.modelClass;
|
|
459
791
|
}
|
|
792
|
+
/**
|
|
793
|
+
* Returns the current transactional session information, if present.
|
|
794
|
+
*/
|
|
795
|
+
getTransaction(options) {
|
|
796
|
+
return options?.transaction ?? transactionContext.getStore();
|
|
797
|
+
}
|
|
460
798
|
/**
|
|
461
799
|
* Creates a new instance of obj scoped to the correct model class or sub-class.
|
|
462
800
|
*/
|
|
@@ -470,55 +808,81 @@ export class RepoUtils {
|
|
|
470
808
|
* Search for existing object based on passed in id and version and product uid.
|
|
471
809
|
*
|
|
472
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.
|
|
473
815
|
*/
|
|
474
|
-
searchIdQuery(id, version) {
|
|
475
|
-
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);
|
|
476
818
|
}
|
|
477
819
|
async truncate(query, options) {
|
|
478
820
|
if (!this.repo) {
|
|
479
821
|
throw new ApiError(ApiErrors.INTERNAL_ERROR, 500, ApiErrorMessages.INTERNAL_ERROR);
|
|
480
822
|
}
|
|
823
|
+
const txInfo = this.getTransaction(options);
|
|
481
824
|
// Check user permissions. Don't check if record-level ACLs are used as this will be done
|
|
482
825
|
// per record later.
|
|
483
826
|
if (this.aclUtils?.enabled && !options.ignoreACL && !this.modelClass.recordACL) {
|
|
484
|
-
if (!(await this.aclUtils.hasPermission(options.user, this.defaultACLUid, ACLAction.
|
|
827
|
+
if (!(await this.aclUtils.hasPermission(options.user, this.defaultACLUid, ACLAction.TRUNCATE))) {
|
|
485
828
|
throw new ApiError(ApiErrors.AUTH_PERMISSION_FAILURE, 403, ApiErrorMessages.AUTH_PERMISSION_FAILURE);
|
|
486
829
|
}
|
|
487
830
|
}
|
|
488
831
|
try {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
if (Array.isArray(query)) {
|
|
492
|
-
uids = await this.repo.distinct("uid", query[0].$match);
|
|
493
|
-
}
|
|
494
|
-
else {
|
|
495
|
-
uids = await this.repo.distinct("uid", query["$match"] ? query["$match"] : query);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
else {
|
|
499
|
-
(await this.repo.find(query)).forEach((obj) => uids.push(obj.uid));
|
|
500
|
-
}
|
|
832
|
+
const searchQuery = ModelUtils.buildSearchQuery(this.modelClass, this.repo, query, true, options?.user);
|
|
833
|
+
const uids = await this.findAllUids(searchQuery, options);
|
|
501
834
|
if (uids.length > 0) {
|
|
502
835
|
let finalUids = uids;
|
|
503
836
|
// Check if this class uses record level ACLs. If so, we need to check the perms of
|
|
504
837
|
// each one. We will remove any from our list that the user does not have permission to
|
|
505
|
-
//
|
|
838
|
+
// truncate.
|
|
506
839
|
if (this.aclUtils?.enabled && this.modelClass.recordACL) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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);
|
|
514
847
|
}
|
|
515
848
|
}
|
|
849
|
+
const cleansUpRecordACLs = !!(this.aclUtils?.enabled && this.modelClass.recordACL);
|
|
516
850
|
// Now delete all records that were found
|
|
517
851
|
if (this.repo instanceof MongoRepository) {
|
|
518
|
-
await this.repo.deleteMany({ uid: { $in: finalUids } }
|
|
852
|
+
await this.repo.deleteMany({ uid: { $in: finalUids } }, {
|
|
853
|
+
session: txInfo?.session,
|
|
854
|
+
});
|
|
519
855
|
}
|
|
520
856
|
else {
|
|
521
|
-
|
|
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
|
+
}
|
|
522
886
|
}
|
|
523
887
|
if (!options?.skipPush) {
|
|
524
888
|
let channels = options?.pushChannels || [];
|
|
@@ -543,6 +907,7 @@ export class RepoUtils {
|
|
|
543
907
|
if (!this.repo) {
|
|
544
908
|
throw new ApiError(ApiErrors.INTERNAL_ERROR, 500, ApiErrorMessages.INTERNAL_ERROR);
|
|
545
909
|
}
|
|
910
|
+
const txInfo = this.getTransaction(options);
|
|
546
911
|
if (this.aclUtils?.enabled && !options?.ignoreACL) {
|
|
547
912
|
const acl = await this.aclUtils.findACL(existing.uid);
|
|
548
913
|
if (!(await this.aclUtils.hasPermission(options?.user, acl ? acl : this.defaultACLUid, ACLAction.UPDATE))) {
|
|
@@ -559,6 +924,15 @@ export class RepoUtils {
|
|
|
559
924
|
if (existing.uid !== obj.uid) {
|
|
560
925
|
throw new ApiError(ApiErrors.OBJECT_ID_MISMATCH, 400, ApiErrorMessages.OBJECT_ID_MISMATCH);
|
|
561
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
|
+
}
|
|
562
936
|
// When using MongoDB we need to copy the _id property in order to prevent duplicate entries
|
|
563
937
|
if (existing instanceof BaseMongoEntity) {
|
|
564
938
|
obj._id = existing._id;
|
|
@@ -569,12 +943,24 @@ export class RepoUtils {
|
|
|
569
943
|
if (this.repo instanceof MongoRepository) {
|
|
570
944
|
if (existing instanceof BaseEntity) {
|
|
571
945
|
if (keepPrevious) {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
+
}
|
|
578
964
|
}
|
|
579
965
|
else {
|
|
580
966
|
await this.repo.updateOne({ uid: obj.uid, version: obj.version }, {
|
|
@@ -583,22 +969,32 @@ export class RepoUtils {
|
|
|
583
969
|
dateModified: new Date(),
|
|
584
970
|
version: obj.version + 1,
|
|
585
971
|
},
|
|
972
|
+
}, {
|
|
973
|
+
session: txInfo?.session,
|
|
586
974
|
});
|
|
587
975
|
}
|
|
588
976
|
}
|
|
589
977
|
else if (obj.uid) {
|
|
590
978
|
if (keepPrevious) {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
+
}
|
|
595
991
|
}
|
|
596
992
|
else {
|
|
597
993
|
await this.repo.updateOne({ uid: obj.uid }, {
|
|
598
994
|
$set: {
|
|
599
995
|
...obj,
|
|
600
996
|
},
|
|
601
|
-
});
|
|
997
|
+
}, { session: txInfo?.session });
|
|
602
998
|
}
|
|
603
999
|
}
|
|
604
1000
|
else {
|
|
@@ -606,20 +1002,21 @@ export class RepoUtils {
|
|
|
606
1002
|
if (keepPrevious) {
|
|
607
1003
|
toSave.version += 1;
|
|
608
1004
|
}
|
|
609
|
-
result = await this.repo.save(toSave);
|
|
1005
|
+
result = await this.repo.save(toSave, { session: txInfo?.session });
|
|
610
1006
|
}
|
|
611
1007
|
}
|
|
612
1008
|
else {
|
|
1009
|
+
const repo = txInfo?.entityManager ? txInfo.entityManager.getRepository(this.modelClass) : this.repo;
|
|
613
1010
|
if (existing instanceof BaseEntity) {
|
|
614
1011
|
if (keepPrevious) {
|
|
615
|
-
await
|
|
1012
|
+
await repo.insert({
|
|
616
1013
|
...obj,
|
|
617
1014
|
dateModified: new Date(),
|
|
618
1015
|
version: obj.version + 1,
|
|
619
1016
|
});
|
|
620
1017
|
}
|
|
621
1018
|
else {
|
|
622
|
-
await
|
|
1019
|
+
await repo.update(query.where, {
|
|
623
1020
|
...obj,
|
|
624
1021
|
dateModified: new Date(),
|
|
625
1022
|
version: obj.version + 1,
|
|
@@ -630,31 +1027,43 @@ export class RepoUtils {
|
|
|
630
1027
|
const toSave = obj;
|
|
631
1028
|
if (keepPrevious) {
|
|
632
1029
|
toSave.version += 1;
|
|
633
|
-
|
|
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);
|
|
634
1034
|
}
|
|
635
1035
|
else {
|
|
636
|
-
await
|
|
1036
|
+
await repo.update(query.where, toSave);
|
|
637
1037
|
}
|
|
638
1038
|
}
|
|
639
1039
|
}
|
|
640
|
-
query = this.searchIdQuery(existing.uid,
|
|
1040
|
+
query = this.searchIdQuery(existing.uid, existing instanceof BaseEntity ? existing.version + 1 : undefined);
|
|
641
1041
|
if (!result) {
|
|
642
1042
|
if (this.repo instanceof MongoRepository) {
|
|
643
|
-
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
|
+
});
|
|
644
1046
|
}
|
|
645
1047
|
else {
|
|
646
|
-
|
|
1048
|
+
const repo = txInfo?.entityManager ? txInfo.entityManager.getRepository(this.modelClass) : this.repo;
|
|
1049
|
+
result = (await repo.findOne(query));
|
|
647
1050
|
}
|
|
648
1051
|
if (!result) {
|
|
649
1052
|
throw new ApiError(ApiErrors.INTERNAL_ERROR, 500, ApiErrorMessages.INTERNAL_ERROR);
|
|
650
1053
|
}
|
|
651
1054
|
}
|
|
652
1055
|
result = this.instantiateObject(result);
|
|
653
|
-
if (result && this.
|
|
654
|
-
// Cache the object for faster retrieval
|
|
655
|
-
|
|
656
|
-
|
|
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));
|
|
657
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);
|
|
658
1067
|
if (!options?.skipPush) {
|
|
659
1068
|
let channels = [result.uid].concat(options?.pushChannels || []);
|
|
660
1069
|
this.notificationUtils?.sendMessage(channels, this.modelClass.name, "update", result);
|
|
@@ -676,18 +1085,31 @@ export class RepoUtils {
|
|
|
676
1085
|
// Instantiate the correct object type so that we can perform validation correctly. If we don't do this
|
|
677
1086
|
// then the provided object will be missing all decorators and validation won't work as desired.
|
|
678
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);
|
|
679
1094
|
ObjectUtils.validate(obj, metadataObj.constructor);
|
|
680
|
-
// Iterate through all properties
|
|
1095
|
+
// Iterate through all properties
|
|
681
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
|
|
682
1104
|
const clazz = Reflect.getMetadata("rrst:reference", metadataObj, member);
|
|
683
|
-
if (clazz && clazz.
|
|
1105
|
+
if (clazz && clazz.datasource && obj[member]) {
|
|
684
1106
|
// Attempt to grab the repository for this reference type
|
|
685
|
-
const conn = this.connectionManager?.connections.get(clazz.
|
|
1107
|
+
const conn = this.connectionManager?.connections.get(clazz.datasource);
|
|
686
1108
|
const repo = conn instanceof MongoConnection || isSqlDataSource(conn)
|
|
687
1109
|
? conn.getRepository(clazz)
|
|
688
1110
|
: undefined;
|
|
689
1111
|
if (repo) {
|
|
690
|
-
// 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
|
|
691
1113
|
// let's make sure that every uid is valid.
|
|
692
1114
|
const uids = Array.isArray(obj[member]) ? obj[member] : [obj[member]];
|
|
693
1115
|
const query = ModelUtils.buildIdSearchQuery(repo, clazz, uids);
|
|
@@ -701,7 +1123,7 @@ export class RepoUtils {
|
|
|
701
1123
|
}
|
|
702
1124
|
}
|
|
703
1125
|
catch (err) {
|
|
704
|
-
throw new ApiError(
|
|
1126
|
+
throw new ApiError(ApiErrors.INVALID_REQUEST, 400, err.message);
|
|
705
1127
|
}
|
|
706
1128
|
}
|
|
707
1129
|
}
|
|
@@ -709,10 +1131,6 @@ __decorate([
|
|
|
709
1131
|
Inject("ACLUtils"),
|
|
710
1132
|
__metadata("design:type", Function)
|
|
711
1133
|
], RepoUtils.prototype, "aclUtils", void 0);
|
|
712
|
-
__decorate([
|
|
713
|
-
RedisConnection("cache"),
|
|
714
|
-
__metadata("design:type", Redis)
|
|
715
|
-
], RepoUtils.prototype, "cacheClient", void 0);
|
|
716
1134
|
__decorate([
|
|
717
1135
|
Config(),
|
|
718
1136
|
__metadata("design:type", Object)
|
|
@@ -725,10 +1143,6 @@ __decorate([
|
|
|
725
1143
|
Logger,
|
|
726
1144
|
__metadata("design:type", Object)
|
|
727
1145
|
], RepoUtils.prototype, "logger", void 0);
|
|
728
|
-
__decorate([
|
|
729
|
-
Inject(ObjectFactory),
|
|
730
|
-
__metadata("design:type", ObjectFactory)
|
|
731
|
-
], RepoUtils.prototype, "objectFactory", void 0);
|
|
732
1146
|
__decorate([
|
|
733
1147
|
Inject(NotificationUtils),
|
|
734
1148
|
__metadata("design:type", NotificationUtils)
|
|
@@ -743,4 +1157,28 @@ __decorate([
|
|
|
743
1157
|
__metadata("design:paramtypes", []),
|
|
744
1158
|
__metadata("design:returntype", Promise)
|
|
745
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);
|
|
746
1184
|
//# sourceMappingURL=RepoUtils.js.map
|