@rapidrest/service-core 2.0.0 → 2.1.1
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/dist/lib/BackgroundServiceManager.js +51 -9
- package/dist/lib/BackgroundServiceManager.js.map +1 -1
- package/dist/lib/EventListenerManager.js +35 -2
- package/dist/lib/EventListenerManager.js.map +1 -1
- package/dist/lib/NetUtils.js +215 -21
- package/dist/lib/NetUtils.js.map +1 -1
- package/dist/lib/ObjectFactory.js +7 -2
- package/dist/lib/ObjectFactory.js.map +1 -1
- package/dist/lib/RateLimiter.js.map +1 -1
- package/dist/lib/Server.js +121 -80
- package/dist/lib/Server.js.map +1 -1
- package/dist/lib/auth/AuthMiddleware.js +160 -100
- package/dist/lib/auth/AuthMiddleware.js.map +1 -1
- package/dist/lib/auth/JWTStrategy.js +6 -2
- package/dist/lib/auth/JWTStrategy.js.map +1 -1
- package/dist/lib/database/ConnectionManager.js +50 -2
- package/dist/lib/database/ConnectionManager.js.map +1 -1
- package/dist/lib/database/DatabaseErrors.js +88 -0
- package/dist/lib/database/DatabaseErrors.js.map +1 -0
- package/dist/lib/database/MongoRepository.js +31 -3
- package/dist/lib/database/MongoRepository.js.map +1 -1
- package/dist/lib/database/MongoSchemaSync.js +7 -1
- package/dist/lib/database/MongoSchemaSync.js.map +1 -1
- package/dist/lib/database/TypeOrmSupport.js +49 -15
- 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/PersistenceDecorators.js +23 -0
- package/dist/lib/decorators/PersistenceDecorators.js.map +1 -1
- package/dist/lib/decorators/RouteDecorators.js +4 -2
- package/dist/lib/decorators/RouteDecorators.js.map +1 -1
- package/dist/lib/http/bun/BunRouter.js +119 -9
- package/dist/lib/http/bun/BunRouter.js.map +1 -1
- package/dist/lib/http/index.js +1 -0
- package/dist/lib/http/index.js.map +1 -1
- package/dist/lib/http/session/sessionMiddleware.js +62 -14
- package/dist/lib/http/session/sessionMiddleware.js.map +1 -1
- package/dist/lib/http/types.js +10 -1
- package/dist/lib/http/types.js.map +1 -1
- package/dist/lib/http/uWS/Adapters.js +31 -13
- package/dist/lib/http/uWS/Adapters.js.map +1 -1
- package/dist/lib/http/uWS/Router.js +80 -16
- package/dist/lib/http/uWS/Router.js.map +1 -1
- package/dist/lib/http/uWS/WebSocket.js +4 -2
- package/dist/lib/http/uWS/WebSocket.js.map +1 -1
- package/dist/lib/models/ModelUtils.js +255 -81
- package/dist/lib/models/ModelUtils.js.map +1 -1
- package/dist/lib/models/RepoUtils.js +863 -266
- package/dist/lib/models/RepoUtils.js.map +1 -1
- package/dist/lib/routes/BaseAdminRoute.js +5 -4
- package/dist/lib/routes/BaseAdminRoute.js.map +1 -1
- package/dist/lib/routes/BasePushRoute.js +104 -40
- package/dist/lib/routes/BasePushRoute.js.map +1 -1
- package/dist/lib/routes/CRUDRoute.js +31 -17
- package/dist/lib/routes/CRUDRoute.js.map +1 -1
- package/dist/lib/routes/RouteUtils.js +120 -29
- package/dist/lib/routes/RouteUtils.js.map +1 -1
- package/dist/lib/security/ACLUtils.js +170 -34
- package/dist/lib/security/ACLUtils.js.map +1 -1
- package/dist/types/BackgroundServiceManager.d.ts +6 -0
- package/dist/types/EventListenerManager.d.ts +4 -0
- package/dist/types/NetUtils.d.ts +65 -6
- package/dist/types/RateLimiter.d.ts +4 -3
- package/dist/types/Server.d.ts +33 -2
- package/dist/types/auth/AuthMiddleware.d.ts +35 -4
- package/dist/types/database/ConnectionManager.d.ts +18 -0
- package/dist/types/database/DatabaseErrors.d.ts +26 -0
- package/dist/types/database/MongoRepository.d.ts +21 -2
- package/dist/types/database/TypeOrmSupport.d.ts +11 -2
- package/dist/types/database/index.d.ts +1 -0
- package/dist/types/decorators/PersistenceDecorators.d.ts +31 -0
- package/dist/types/decorators/RouteDecorators.d.ts +4 -2
- package/dist/types/http/bun/BunRouter.d.ts +23 -2
- package/dist/types/http/index.d.ts +2 -1
- package/dist/types/http/session/sessionMiddleware.d.ts +14 -4
- package/dist/types/http/types.d.ts +40 -0
- package/dist/types/http/uWS/Adapters.d.ts +10 -1
- package/dist/types/http/uWS/Router.d.ts +15 -2
- package/dist/types/models/ModelUtils.d.ts +96 -1
- package/dist/types/models/RepoUtils.d.ts +240 -4
- package/dist/types/routes/BasePushRoute.d.ts +5 -0
- package/dist/types/routes/CRUDRoute.d.ts +10 -0
- package/dist/types/routes/RouteUtils.d.ts +37 -1
- package/dist/types/security/ACLUtils.d.ts +68 -7
- package/package.json +1 -1
|
@@ -36,6 +36,25 @@ export interface RepoOperationOptions {
|
|
|
36
36
|
export interface RepoCreateOptions extends RepoOperationOptions {
|
|
37
37
|
/** The AccessControlList to use when creating a new object. */
|
|
38
38
|
acl?: AccessControlList;
|
|
39
|
+
/**
|
|
40
|
+
* Set to `true` to let a record-level-ACL create adopt an ACL that already exists at the new record's uid even
|
|
41
|
+
* though no record of this model exists there yet and the caller doesn't already hold every creator right on
|
|
42
|
+
* it. The existing ACL is used as-is (the creator is not added to it). Defaults to `false`, in which case such
|
|
43
|
+
* a create is refused with `IDENTIFIER_EXISTS` - see `create()`.
|
|
44
|
+
*
|
|
45
|
+
* For trusted server-side code only (e.g. code that deliberately saves a record's ACL before the record
|
|
46
|
+
* itself) - this must never be derived from a client request.
|
|
47
|
+
*/
|
|
48
|
+
allowExistingACL?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Set to `true` to keep the `_id` carried by the object being created instead of discarding it. The object is
|
|
51
|
+
* still always inserted, never merged into an existing document: a create whose `_id` is already taken fails
|
|
52
|
+
* with `IDENTIFIER_EXISTS`. Defaults to `false`.
|
|
53
|
+
*
|
|
54
|
+
* For trusted server-side code only (e.g. restoring or migrating documents with their original `_id`) - this
|
|
55
|
+
* must never be derived from a client request.
|
|
56
|
+
*/
|
|
57
|
+
preserveId?: boolean;
|
|
39
58
|
}
|
|
40
59
|
/**
|
|
41
60
|
* The available options for the `RepoUtils.delete()` operation.
|
|
@@ -73,6 +92,18 @@ export interface RepoFindOptions extends RepoOperationOptions {
|
|
|
73
92
|
export interface RepoUpdateOptions<T extends BaseEntity | SimpleEntity> extends RepoOperationOptions {
|
|
74
93
|
/** The desired version number of the resource to update. */
|
|
75
94
|
version?: number | string;
|
|
95
|
+
/**
|
|
96
|
+
* Set to `true` to let this update actually write `@ReadOnly` fields instead of unconditionally
|
|
97
|
+
* resetting them back to their existing persisted value. Defaults to `false` (the field is protected),
|
|
98
|
+
* matching this method's behavior prior to this option's introduction.
|
|
99
|
+
*
|
|
100
|
+
* For trusted server-side code only - this must never be derived from a client request. It exists for
|
|
101
|
+
* a caller that legitimately owns a `@ReadOnly` field's lifecycle outside the ordinary create/update
|
|
102
|
+
* path (e.g. a background job or route handler computing and persisting a system-managed value), where
|
|
103
|
+
* the alternative would be bypassing `RepoUtils.update()` entirely (losing its ACL/optimistic-locking/
|
|
104
|
+
* transaction handling) just to change that one field.
|
|
105
|
+
*/
|
|
106
|
+
allowReadOnly?: boolean;
|
|
76
107
|
}
|
|
77
108
|
/**
|
|
78
109
|
* @author Jean-Philippe Steinmetz
|
|
@@ -95,18 +126,46 @@ export declare class RepoUtils<T extends BaseEntity | SimpleEntity> {
|
|
|
95
126
|
constructor(modelClass: any, repo?: Repository<T> | MongoRepository<T>);
|
|
96
127
|
protected init(): Promise<void>;
|
|
97
128
|
/**
|
|
98
|
-
* Retrieves
|
|
99
|
-
* baked into it by `ModelUtils.buildSearchQuery
|
|
100
|
-
*
|
|
101
|
-
* `
|
|
129
|
+
* Retrieves the uids matching the given (already-built) search query, ignoring any pagination `take`/`page`
|
|
130
|
+
* baked into it by `ModelUtils.buildSearchQuery`, unless `cap` is given.
|
|
131
|
+
*
|
|
132
|
+
* Without `cap` the whole matching set is returned. Pass `cap` whenever each uid will cost a per-record ACL check
|
|
133
|
+
* on behalf of a client that acts on the records (`truncate()` on a `recordACL` model): otherwise a single anonymous
|
|
134
|
+
* request can trigger unbounded ACL work, and on MongoDB a `distinct` over enough uids exceeds the 16MB reply limit.
|
|
135
|
+
* `count()` must report the true total, so it never uses this - see `countPermittedUids()`.
|
|
136
|
+
*
|
|
137
|
+
* @param cap The maximum number of uids to return.
|
|
102
138
|
*/
|
|
103
139
|
private findAllUids;
|
|
140
|
+
/**
|
|
141
|
+
* Returns the maximum number of records a client request may have checked individually against record-level
|
|
142
|
+
* ACLs: the `take` that `ModelUtils.buildSearchQuerySQL()` already resolved onto the built SQL query. A MongoDB
|
|
143
|
+
* query carries no `take`, so the same rule is read from `ModelUtils.resolvePagination()`, the helper that
|
|
144
|
+
* documents it for MongoDB callers.
|
|
145
|
+
*/
|
|
146
|
+
private recordACLCap;
|
|
147
|
+
/**
|
|
148
|
+
* Determines whether a built search query can match soft-deleted records: every OR branch must pin `deleted` to
|
|
149
|
+
* exactly `false` for it not to. Decided from the compiled query rather than the raw client value, so any
|
|
150
|
+
* spelling of the filter (`true`, `eq(true)`, `in(true,false)`, `ne(false)`, a repeated parameter, a `$or`
|
|
151
|
+
* branch) is recognized. Always `false` for a model that isn't recoverable.
|
|
152
|
+
*/
|
|
153
|
+
private queryIncludesDeleted;
|
|
104
154
|
/**
|
|
105
155
|
* Filters the given uids down to those the user has `action` permission for, checking in bounded-size
|
|
106
156
|
* batches rather than a single unbounded `Promise.all` so a large matching set can't fire an unbounded
|
|
107
157
|
* number of concurrent permission-check round trips at once.
|
|
108
158
|
*/
|
|
109
159
|
private filterPermittedUids;
|
|
160
|
+
/**
|
|
161
|
+
* Counts the records matched by an (already-built) search query that the caller holds the given permission(s) on,
|
|
162
|
+
* for a `recordACL` model. Unlike `findAllUids()` this is never capped - a count must be the true total - but it
|
|
163
|
+
* never holds the whole matching set either: MongoDB streams uids from a projected cursor instead of a `distinct`
|
|
164
|
+
* (whose single reply is limited to 16MB), and permissions are checked in bounded batches as the uids arrive.
|
|
165
|
+
*
|
|
166
|
+
* @param actions Every action the caller must hold on a record for it to be counted.
|
|
167
|
+
*/
|
|
168
|
+
private countPermittedUids;
|
|
110
169
|
count(query: any, options?: RepoFindOptions): Promise<number>;
|
|
111
170
|
/**
|
|
112
171
|
* Determines whether an object with the given unique identifier (and, optionally, a specific version) exists
|
|
@@ -153,6 +212,85 @@ export declare class RepoUtils<T extends BaseEntity | SimpleEntity> {
|
|
|
153
212
|
* @param options The additional options to consider during the search.
|
|
154
213
|
*/
|
|
155
214
|
findOne(id: string, options?: RepoFindOptions): Promise<T | undefined>;
|
|
215
|
+
/**
|
|
216
|
+
* Resolves the per-record ACL for a record about to be created by `create()` under `uid`. Returns `uid` when a
|
|
217
|
+
* fresh ACL was created for it (so the caller can clean it up if the create fails), or `undefined` when an
|
|
218
|
+
* existing ACL is legitimately reused as-is.
|
|
219
|
+
*
|
|
220
|
+
* ACLs live in one global collection keyed only by uid, shared by every model and by the class/route ACLs, and a
|
|
221
|
+
* create's uid can come from the client. An ACL that already exists at `uid` may guard a record of another model,
|
|
222
|
+
* a whole model or route, or have been planted there by another user ahead of time - adopting it would hand
|
|
223
|
+
* whoever holds rights on it the new record, or hand the creator whatever it protects. So:
|
|
224
|
+
* - a code-defined ACL uid (a class, route or endpoint ACL, or any `default_*` uid; see
|
|
225
|
+
* `ACLUtils.isReservedUid()`/`isProtectedACL()`) is never claimed or reused, whoever the caller is;
|
|
226
|
+
* - an existing ACL is reused, unchanged, only for a trackChanges "new version" of an existing record
|
|
227
|
+
* (`count > 0`; `create()` has already verified the caller's UPDATE right on that record), or when trusted
|
|
228
|
+
* server code passed `allowExistingACL`. Neither a trusted role nor already holding rights on it is enough.
|
|
229
|
+
*
|
|
230
|
+
* Otherwise the create is refused with `IDENTIFIER_EXISTS`, the same error as a record identifier collision.
|
|
231
|
+
* A genuinely orphaned ACL can't be reliably told apart from one guarding another model's record, so no attempt
|
|
232
|
+
* is made to replace it. A fresh ACL is claimed with `saveACL()`'s `createOnly` mode, so two concurrent creates
|
|
233
|
+
* (of the same or of different models) can't both claim the same uid either.
|
|
234
|
+
*/
|
|
235
|
+
private claimRecordACL;
|
|
236
|
+
/**
|
|
237
|
+
* Converts every `Date`-typed property of `obj` that holds a string or number (e.g. an ISO 8601 string from a
|
|
238
|
+
* JSON request body) to a real `Date`, in place. Without this a MongoDB document stores the string itself, and
|
|
239
|
+
* date range queries (which compare against `Date` operands) never match it.
|
|
240
|
+
*
|
|
241
|
+
* A property counts as `Date`-typed when its `@Column` declares an explicit date/time `type`, or otherwise when
|
|
242
|
+
* TypeScript's emitted `design:type` is `Date`. TypeScript reflects a union-typed property (e.g. `Date | null`)
|
|
243
|
+
* as `Object`, so such a property is only converted when its `@Column` sets `type` explicitly. Properties that
|
|
244
|
+
* aren't `@Column`s are never touched.
|
|
245
|
+
*
|
|
246
|
+
* Accepted values (anything else, including a `Date` column holding a boolean or an object, is a 400):
|
|
247
|
+
* - an ISO 8601 date (`YYYY-MM-DD`) or date-time (`YYYY-MM-DDTHH:mm[:ss[.fff]]`, `T` or a space), with a zone
|
|
248
|
+
* designator of `Z`, `±HH`, `±HHmm` or `±HH:mm`. A date-time without a zone is read as UTC, never as the server's
|
|
249
|
+
* local time;
|
|
250
|
+
* - a finite number of epoch milliseconds between years 1 and 9999 whose magnitude is at least `1e11` (a smaller
|
|
251
|
+
* number is ambiguous with epoch seconds). Numeric strings are not accepted;
|
|
252
|
+
* - a `Date`, `null` or `undefined`, which are left as they are.
|
|
253
|
+
*
|
|
254
|
+
* A SQL date-only column (`@Column({ type: "date" })`) is validated as a `YYYY-MM-DD` string but kept as that
|
|
255
|
+
* string: TypeORM writes a `Date` into such a column using the server's local calendar date, so converting it
|
|
256
|
+
* would store the previous day on any server west of UTC. On MongoDB, which has no date-only type, it is
|
|
257
|
+
* converted to midnight UTC like any other date.
|
|
258
|
+
*
|
|
259
|
+
* @param obj The object whose properties to convert.
|
|
260
|
+
* @param clazz The model class describing `obj`.
|
|
261
|
+
* @throws ApiError `INVALID_REQUEST` (400) when a value isn't a valid date.
|
|
262
|
+
*/
|
|
263
|
+
protected coerceDateProperties(obj: any, clazz: any): void;
|
|
264
|
+
/**
|
|
265
|
+
* Returns the date-typed `@Column`s of `clazz` (see `coerceDateProperties()`), computed once per class.
|
|
266
|
+
*/
|
|
267
|
+
private static getDateColumns;
|
|
268
|
+
/**
|
|
269
|
+
* Parses a client-supplied date value: an ISO 8601 string or a number of epoch milliseconds (see
|
|
270
|
+
* `coerceDateProperties()` for the exact rules). Returns `undefined` for anything else.
|
|
271
|
+
*/
|
|
272
|
+
private static parseDateInput;
|
|
273
|
+
/**
|
|
274
|
+
* Strictly parses an ISO 8601 date (`YYYY-MM-DD`) or, unless `dateOnly`, date-time string, rejecting impossible
|
|
275
|
+
* calendar values (e.g. `2026-02-30`) that `new Date()` would silently roll over. A date-time without a zone is
|
|
276
|
+
* read as UTC.
|
|
277
|
+
*/
|
|
278
|
+
private static parseISODate;
|
|
279
|
+
/**
|
|
280
|
+
* Rejects update input with a top-level key that MongoDB would interpret as something other than a plain field
|
|
281
|
+
* name: a dotted path (`"aliases.3"`, which writes a nested element) or an operator (`"$inc"`). Such a key
|
|
282
|
+
* bypasses route/model validation (which only knows the model's real property names), so it is refused on
|
|
283
|
+
* every backend rather than passed through.
|
|
284
|
+
*
|
|
285
|
+
* @throws ApiError `INVALID_REQUEST` (400) naming the first offending key.
|
|
286
|
+
*/
|
|
287
|
+
private assertPlainPropertyNames;
|
|
288
|
+
/**
|
|
289
|
+
* Determines whether `existing` is under optimistic locking: a `BaseEntity` instance or - for a `BaseEntity`
|
|
290
|
+
* model - any object carrying a numeric `version`, such as a plain document read straight from a
|
|
291
|
+
* `MongoRepository` (whose `find()`/`findOne()` return plain documents, not model instances).
|
|
292
|
+
*/
|
|
293
|
+
private isVersioned;
|
|
156
294
|
/**
|
|
157
295
|
* Returns the default access control list governing the model type. Returning a value of `undefined` will grant
|
|
158
296
|
* full acccess to any user (including unauthenticated anonymous users).
|
|
@@ -208,6 +346,104 @@ export declare class RepoUtils<T extends BaseEntity | SimpleEntity> {
|
|
|
208
346
|
searchIdQuery(id: string, version?: number | string, includeDeleted?: boolean): any;
|
|
209
347
|
truncate(query: any, options: RepoFindOptions): Promise<void>;
|
|
210
348
|
update(obj: Partial<T>, existing: T, options?: RepoUpdateOptions<T>): Promise<T>;
|
|
349
|
+
/**
|
|
350
|
+
* Maps an error thrown by one of `update()`'s writes: a duplicate key on the record's identity (`_id`/primary key
|
|
351
|
+
* or `(uid, version)`) is a lost optimistic-lock race (409 `INVALID_OBJECT_VERSION`), and a duplicate value of any
|
|
352
|
+
* other unique column is a 400 `IDENTIFIER_EXISTS`. Any other error is returned as is.
|
|
353
|
+
*
|
|
354
|
+
* @param err The error thrown by the write.
|
|
355
|
+
* @param versionInsert Whether the write inserted a new version (`trackChanges`), which makes an unidentified
|
|
356
|
+
* duplicate key most likely a version clash.
|
|
357
|
+
*/
|
|
358
|
+
private mapUpdateWriteError;
|
|
359
|
+
/**
|
|
360
|
+
* Counts the stored rows/documents (every version, deleted or not) whose identifier matches `id`, ignoring ACLs.
|
|
361
|
+
*
|
|
362
|
+
* @param id The identifier, or identifiers, to match.
|
|
363
|
+
* @param txInfo The active transaction, if any.
|
|
364
|
+
* @param clazz The model class to build the identifier query for.
|
|
365
|
+
*/
|
|
366
|
+
private countById;
|
|
367
|
+
/**
|
|
368
|
+
* Reads the stored version of `existing` straight from the database, without ACL checks, caching or scoped
|
|
369
|
+
* property removal. Used by `update()` when `existing` lacks fields it must carry over.
|
|
370
|
+
*/
|
|
371
|
+
private loadStoredRecord;
|
|
372
|
+
/** Matches a whole-word `me` anywhere in a serialized query (see `find()`'s cache key). */
|
|
373
|
+
private static readonly REGEX_ME;
|
|
374
|
+
/**
|
|
375
|
+
* Returns whether this model keeps every version of a record as its own immutable row/document (`trackChanges`),
|
|
376
|
+
* which is what makes a version-specific cache entry safe to keep until it expires.
|
|
377
|
+
*/
|
|
378
|
+
private get versionsAreImmutable();
|
|
379
|
+
/** Parses a `version` option the way `searchIdQuery()` does. */
|
|
380
|
+
private parseVersion;
|
|
381
|
+
/**
|
|
382
|
+
* Returns the cache key of a single record: its latest version, or (only for a model whose versions are immutable)
|
|
383
|
+
* a specific version. Returns `undefined` for a specific version of any other model, which is never cached: its
|
|
384
|
+
* entry would outlive the in-place update that replaces that version.
|
|
385
|
+
*
|
|
386
|
+
* Record keys (`rec:`) and query result keys (`q:`) are distinct namespaces, so no client-chosen uid can ever name
|
|
387
|
+
* a query result entry (or the other way around).
|
|
388
|
+
*/
|
|
389
|
+
private recordCacheKey;
|
|
390
|
+
/** Returns the cache key of a `find()` result for the given (JSON-serializable) key material. */
|
|
391
|
+
private queryCacheKey;
|
|
392
|
+
/**
|
|
393
|
+
* Determines whether a cached record really is the one identified by `id` (and `version`, when given): one of the
|
|
394
|
+
* model's identifier properties must hold `id`. Guards against a cache entry written under a colliding key.
|
|
395
|
+
*/
|
|
396
|
+
private matchesId;
|
|
397
|
+
/**
|
|
398
|
+
* Returns a shallow copy of `record` with the same prototype, for handing out (or stripping) without altering the
|
|
399
|
+
* object a cache holds.
|
|
400
|
+
*/
|
|
401
|
+
private copyRecord;
|
|
402
|
+
/**
|
|
403
|
+
* Stores a copy of `record` in the cache under its latest-version key, and under its version key when versions are
|
|
404
|
+
* immutable. Fire-and-forget: a cache failure never fails the write.
|
|
405
|
+
*/
|
|
406
|
+
private cacheRecord;
|
|
407
|
+
/**
|
|
408
|
+
* Caches a page of `find()` results: each record under its own record key, and the query key as the list of those
|
|
409
|
+
* records' `[uid, version]` references. A record of a model whose versions are immutable is referenced by its
|
|
410
|
+
* version (a list may hold past versions); any other record by its latest-version key, which `update()` refreshes
|
|
411
|
+
* and `delete()` removes, so a cached list never serves a stale or deleted record.
|
|
412
|
+
*/
|
|
413
|
+
private cacheResults;
|
|
414
|
+
/**
|
|
415
|
+
* Loads a cached page of `find()` results (see `cacheResults()`). Records that have since expired, been deleted, or
|
|
416
|
+
* whose entry isn't the referenced record are left out; an empty array means nothing usable was cached.
|
|
417
|
+
*/
|
|
418
|
+
private loadCachedResults;
|
|
419
|
+
/**
|
|
420
|
+
* Returns the version-specific cache keys of every stored version of the given records (only for a model whose
|
|
421
|
+
* versions are cached individually; otherwise none). Must be called before the records are removed.
|
|
422
|
+
*/
|
|
423
|
+
private versionCacheKeys;
|
|
424
|
+
/**
|
|
425
|
+
* Removes the cached entries of the given records: their latest-version keys plus `versionKeys` (see
|
|
426
|
+
* `versionCacheKeys()`). Fire-and-forget: a cache failure never fails the write.
|
|
427
|
+
*/
|
|
428
|
+
private uncacheRecords;
|
|
429
|
+
/**
|
|
430
|
+
* Returns whether this is the ACL model itself (served by e.g. a `BaseACLRoute`), whose documents `ACLUtils`
|
|
431
|
+
* caches separately under their uids.
|
|
432
|
+
*/
|
|
433
|
+
private get isACLModel();
|
|
434
|
+
/**
|
|
435
|
+
* After a write to the ACL model, drops `ACLUtils`' cached copies of the written ACLs so permission checks see the
|
|
436
|
+
* change right away (they would otherwise keep using the old ACL until its cache entry expired). A failure is
|
|
437
|
+
* logged rather than failing the write, which has already happened.
|
|
438
|
+
*/
|
|
439
|
+
private invalidateACLCache;
|
|
440
|
+
/**
|
|
441
|
+
* Sends a push notification about a write, unless `skipPush` is set. The payload is a copy with every
|
|
442
|
+
* `@RequiresScope` property removed: subscribers only need READ on the record's channel, and the writer's scopes
|
|
443
|
+
* say nothing about theirs, so no scoped value is ever published. Consumers that need a scoped value must fetch
|
|
444
|
+
* the record, which applies their own scopes.
|
|
445
|
+
*/
|
|
446
|
+
private publish;
|
|
211
447
|
/**
|
|
212
448
|
* Performs validation on the object(s) provided. This function first calls `ObjectUtils.validate()` to check
|
|
213
449
|
* any class level defined validation functions. Second, it scans for any properties with the `@Reference`
|
|
@@ -48,6 +48,11 @@ export declare class BasePushRoute {
|
|
|
48
48
|
* clobber (or exceed the cap on) each other's committed subscription list.
|
|
49
49
|
*/
|
|
50
50
|
private runExclusive;
|
|
51
|
+
/**
|
|
52
|
+
* Returns `true` if the given socket is closing or closed. Works for the `ws` library's sockets as well as the
|
|
53
|
+
* uWS and Bun shims, which all use the WebSocket `readyState` values (2 = CLOSING, 3 = CLOSED).
|
|
54
|
+
*/
|
|
55
|
+
private isSocketClosed;
|
|
51
56
|
connect(sock: ws, user: any): Promise<void>;
|
|
52
57
|
send(id: string, msg: any, user: any): Promise<void>;
|
|
53
58
|
}
|
|
@@ -38,6 +38,16 @@ export declare abstract class CRUDRoute<T extends BaseEntity | SimpleEntity> ext
|
|
|
38
38
|
*/
|
|
39
39
|
protected validateCreate(obj: Partial<T>, user?: JWTUser): Promise<void>;
|
|
40
40
|
private validateCreateBulk;
|
|
41
|
+
/**
|
|
42
|
+
* Returns `err` if it is an `ApiError`, otherwise a generic `INVALID_REQUEST` error. A validator's `ApiError` is
|
|
43
|
+
* meant for the client, but any other error (e.g. a driver error from a custom validator) may expose internals.
|
|
44
|
+
*/
|
|
45
|
+
private static toValidationError;
|
|
46
|
+
/**
|
|
47
|
+
* Throws a `BulkError` when at least one object in a bulk request failed validation. The `BulkError` holds one
|
|
48
|
+
* entry per object, in request order: `null` for an object that passed, otherwise the reason it failed.
|
|
49
|
+
*/
|
|
50
|
+
private static throwBulkValidationErrors;
|
|
41
51
|
create(obj: T | T[], req: HttpRequest, user?: JWTUser): Promise<T | Array<T>>;
|
|
42
52
|
delete(id: string, version: string | undefined, purge: string | undefined, req: HttpRequest, user?: JWTUser): Promise<void>;
|
|
43
53
|
exists(id: string, query: any, res: HttpResponse, user?: JWTUser): Promise<any>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RequestHandler } from "../http/types.js";
|
|
1
|
+
import type { HttpRequest, RequestHandler } from "../http/types.js";
|
|
2
2
|
import { RateLimiter } from "../RateLimiter.js";
|
|
3
3
|
import { RateLimitOptions } from "../decorators/RouteDecorators.js";
|
|
4
4
|
/**
|
|
@@ -13,13 +13,37 @@ export declare class RouteUtils {
|
|
|
13
13
|
private logger?;
|
|
14
14
|
protected rateLimiter?: RateLimiter;
|
|
15
15
|
protected trustedRoles: string[];
|
|
16
|
+
protected trustedProxies: string[];
|
|
16
17
|
/**
|
|
17
18
|
* Creates a middleware function that checks if the user has elevated privileges and if not
|
|
18
19
|
* throws the `AUTH_REQUIRES_ELEVATION` error.
|
|
19
20
|
*/
|
|
20
21
|
checkElevation(lastStart?: number): RequestHandler;
|
|
22
|
+
/**
|
|
23
|
+
* Returns a canonical form of a request path for use in rate limit identifiers. The router percent-decodes
|
|
24
|
+
* `:param` values, so `/clients/%61bc` and `/clients/abc` reach the same handler. Keying on the raw path would
|
|
25
|
+
* give an attacker a separate bucket for every encoding variant. Each segment is decoded and then re-encoded
|
|
26
|
+
* (a malformed segment is kept as is), and empty segments from repeated or trailing slashes are dropped.
|
|
27
|
+
*
|
|
28
|
+
* @param path The request path to normalize.
|
|
29
|
+
*/
|
|
30
|
+
static normalizeRateLimitPath(path: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Returns the path part of a rate limit identifier for `req`. When the router has set `req.routePattern`, the
|
|
33
|
+
* pattern is used with each `:param` segment replaced by its decoded value from `req.params`, re-encoded. A
|
|
34
|
+
* pattern with a `*` wildcard can't be rebuilt from params, so it and a request without a pattern fall back to
|
|
35
|
+
* `normalizeRateLimitPath(req.path)`.
|
|
36
|
+
*
|
|
37
|
+
* @param req The request to build the identifier path for.
|
|
38
|
+
*/
|
|
39
|
+
static getRateLimitPath(req: HttpRequest): string;
|
|
21
40
|
/**
|
|
22
41
|
* Creates a middleware function that performs rate limiting on the request.
|
|
42
|
+
*
|
|
43
|
+
* The identifier is `options.id` when set. Otherwise it is the request method plus the matched route pattern with
|
|
44
|
+
* its decoded params, or the normalized request path (see `getRateLimitPath()`). With `options.perUser`, an authenticated caller gets a bucket keyed on their
|
|
45
|
+
* uid and an anonymous caller gets a bucket keyed on their client IP address, so one anonymous client can't
|
|
46
|
+
* exhaust the limit for every other anonymous caller.
|
|
23
47
|
*/
|
|
24
48
|
checkRateLimiter(options: RateLimitOptions): RequestHandler;
|
|
25
49
|
/**
|
|
@@ -63,6 +87,18 @@ export declare class RouteUtils {
|
|
|
63
87
|
* @returns The list of `@Method` decorated functions that were found.
|
|
64
88
|
*/
|
|
65
89
|
getRouteMethods(route: any): Map<string, any>;
|
|
90
|
+
/**
|
|
91
|
+
* Persists a `@Protect` ACL declared on a route class or handler.
|
|
92
|
+
*
|
|
93
|
+
* Registration fails closed. If the ACL can't be saved (for example the `acl` datastore is unavailable at
|
|
94
|
+
* startup), the error is rethrown so the route is never registered. If it saves but `saveDefaultACL()`
|
|
95
|
+
* returns no user-editable ACL, an error is logged. The route is still registered with its permission check
|
|
96
|
+
* keyed on the declared uid, and `ACLUtils.checkRequestPerms()` denies every request until that ACL exists.
|
|
97
|
+
*
|
|
98
|
+
* @param aclUtils The ACL utilities to save with.
|
|
99
|
+
* @param acl The ACL declared by `@Protect`.
|
|
100
|
+
*/
|
|
101
|
+
private saveRouteACL;
|
|
66
102
|
/**
|
|
67
103
|
* Registers the provided route object containing a set of decorated endpoints to the server.
|
|
68
104
|
*
|
|
@@ -13,6 +13,11 @@ export declare class ACLUtils {
|
|
|
13
13
|
private cache?;
|
|
14
14
|
private connMgr?;
|
|
15
15
|
private logger?;
|
|
16
|
+
/**
|
|
17
|
+
* The uids of every code-defined (class, route and endpoint) ACL registered via `saveDefaultACL()` in this
|
|
18
|
+
* process, together with their `default_<uid>` counterparts. See `isReservedUid()`.
|
|
19
|
+
*/
|
|
20
|
+
private reservedUids;
|
|
16
21
|
private trustedRoles;
|
|
17
22
|
private get repo();
|
|
18
23
|
private init;
|
|
@@ -25,6 +30,30 @@ export declare class ACLUtils {
|
|
|
25
30
|
* match, or `"none"` if the user doesn't match this id at all.
|
|
26
31
|
*/
|
|
27
32
|
private matchSpecificity;
|
|
33
|
+
/**
|
|
34
|
+
* Determines whether `uid` names a code-defined ACL: any `default_<uid>` ACL, or a class/route/endpoint ACL (or
|
|
35
|
+
* its `default_` counterpart) registered through `saveDefaultACL()`. Such an ACL governs a whole model or route,
|
|
36
|
+
* so a record must never be created at, adopt, or remove it (see `RepoUtils.create()`/`delete()`/`truncate()`).
|
|
37
|
+
*
|
|
38
|
+
* @param uid The ACL uid to check.
|
|
39
|
+
*/
|
|
40
|
+
isReservedUid(uid: string | undefined): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Determines whether `acl` is a code-defined ACL: its uid is reserved (see `isReservedUid()`), or it has the
|
|
43
|
+
* shape of the user-editable half of a default ACL (its parent is `default_<uid>`). The latter also catches one
|
|
44
|
+
* registered by a route or model that this process doesn't load.
|
|
45
|
+
*
|
|
46
|
+
* @param acl The ACL to check.
|
|
47
|
+
*/
|
|
48
|
+
isProtectedACL(acl: AccessControlList | undefined | null): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Drops the cached copies of the given ACLs, so the next permission check reads them from the database. Any code
|
|
51
|
+
* that writes ACL documents without going through `saveACL()`/`removeACL()` must call this (e.g. `RepoUtils`
|
|
52
|
+
* serving the ACL REST routes).
|
|
53
|
+
*
|
|
54
|
+
* @param uids The uids of the ACLs to invalidate.
|
|
55
|
+
*/
|
|
56
|
+
invalidateACLs(uids: string[]): Promise<void>;
|
|
28
57
|
/**
|
|
29
58
|
* Validates that the user has permission to perform the request operation against the URL path for the
|
|
30
59
|
* provided request. If ACLUtils has not been initialized or the `acl` datasource has not been configured
|
|
@@ -55,11 +84,13 @@ export declare class ACLUtils {
|
|
|
55
84
|
* @param entityId The unique identifier of the ACL to retrieve.
|
|
56
85
|
* @param parentUids The list of already found parent UIDs. This is used to break circular dependencies.
|
|
57
86
|
* @param options Set `skipCache: true` to bypass the cache and always read the current database state —
|
|
58
|
-
* mirrors `RepoUtils`'s `RepoFindOptions.skipCache
|
|
59
|
-
* afterward
|
|
87
|
+
* mirrors `RepoUtils`'s `RepoFindOptions.skipCache` (applies to the whole parent chain). A database read is
|
|
88
|
+
* written back into the cache afterward. Set `skipParents: true` to not populate the parent chain at all, when
|
|
89
|
+
* only the ACL itself is needed.
|
|
60
90
|
*/
|
|
61
91
|
findACL(entityId: string, parentUids?: string[], options?: {
|
|
62
92
|
skipCache?: boolean;
|
|
93
|
+
skipParents?: boolean;
|
|
63
94
|
}): Promise<AccessControlList | undefined>;
|
|
64
95
|
/**
|
|
65
96
|
* Deletes the ACL with the given identifier from the database, returning the document that was actually
|
|
@@ -77,9 +108,16 @@ export declare class ACLUtils {
|
|
|
77
108
|
* actually belongs to the same physical connection this repo does. Reusing it directly used to throw
|
|
78
109
|
* (Mongo) or silently target the wrong connection (SQL).
|
|
79
110
|
*
|
|
111
|
+
* Pass `unlessProtected: true` to leave a code-defined ACL (see `isProtectedACL()`) in place, resolving to
|
|
112
|
+
* `undefined` as if there had been nothing to remove. `RepoUtils` uses this when cleaning up after a record, so a
|
|
113
|
+
* record that shares a class/route ACL's uid can never delete that ACL.
|
|
114
|
+
*
|
|
80
115
|
* @param uid The unique identifier of the ACL to remove.
|
|
116
|
+
* @param options Set `unlessProtected: true` to never remove a code-defined ACL.
|
|
81
117
|
*/
|
|
82
|
-
removeACL(uid: string
|
|
118
|
+
removeACL(uid: string, options?: {
|
|
119
|
+
unlessProtected?: boolean;
|
|
120
|
+
}): Promise<AccessControlList | undefined>;
|
|
83
121
|
/**
|
|
84
122
|
* Compares two ACLs to see if they have been modified and returns the total number of changes between them.
|
|
85
123
|
*
|
|
@@ -106,14 +144,27 @@ export declare class ACLUtils {
|
|
|
106
144
|
* need the entity-side write and this ACL save to stay consistent should register a compensating action
|
|
107
145
|
* via `registerRollbackHook()`.
|
|
108
146
|
*
|
|
147
|
+
* Pass `createOnly: true` to claim a brand new ACL: it is only ever inserted (at version `0`), never merged
|
|
148
|
+
* into or overwritten on top of an ACL that already exists at the same uid. If one already exists - including
|
|
149
|
+
* one a concurrent caller inserted first, caught by the ACL collection's unique key - an `IDENTIFIER_EXISTS`
|
|
150
|
+
* `ApiError` (400) is thrown instead. Used by `RepoUtils.create()` so that a create can never adopt (and grant
|
|
151
|
+
* its creator rights on) an ACL that belongs to some other record.
|
|
152
|
+
*
|
|
109
153
|
* @param acl The ACL to store.
|
|
110
154
|
* @param options Set `preserveVersion: true` to restore `acl` exactly as given (see above) instead of the
|
|
111
|
-
* normal optimistic-locking update semantics.
|
|
155
|
+
* normal optimistic-locking update semantics. Set `createOnly: true` to only ever insert a new ACL (see above).
|
|
112
156
|
* @return Returns the ACL that was stored in the database.
|
|
113
157
|
*/
|
|
114
158
|
saveACL(acl: AccessControlList, options?: {
|
|
115
159
|
preserveVersion?: boolean;
|
|
160
|
+
createOnly?: boolean;
|
|
116
161
|
}): Promise<AccessControlList | null>;
|
|
162
|
+
/**
|
|
163
|
+
* Implements `saveACL()`'s `createOnly` mode: inserts `acl` as a brand new version `0` document, refusing with
|
|
164
|
+
* `IDENTIFIER_EXISTS` if an ACL already exists at its uid (checked up front, and again via the unique key if a
|
|
165
|
+
* concurrent insert wins the race in between).
|
|
166
|
+
*/
|
|
167
|
+
private insertNewACL;
|
|
117
168
|
/**
|
|
118
169
|
* Stores the given default access control list into the ACL database. A default ACL is a special type of ACL
|
|
119
170
|
* that is primarily defined and maintained within the code but allows for user-specific overrides. To accomplish
|
|
@@ -123,8 +174,15 @@ export declare class ACLUtils {
|
|
|
123
174
|
* overrides. As the `default_<uid>` record is always overwritten with the lastest version of the code, any
|
|
124
175
|
* user-defined changes made to it are lost on service restart.
|
|
125
176
|
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
177
|
+
* Both uids are reserved (see `isReservedUid()`), so no record can be created at, adopt or remove either ACL.
|
|
178
|
+
*
|
|
179
|
+
* Contract: when RBAC is enabled and `acl` is given, this either resolves to the user-editable ACL (whose `uid`
|
|
180
|
+
* is `acl.uid`) or throws; it never resolves to `null` then. A missing user-editable ACL (e.g. one deleted since
|
|
181
|
+
* the last start) is recreated, with no records, under `default_<uid>`. It resolves to `null` only when RBAC is
|
|
182
|
+
* disabled or no `acl` is given, and then stores nothing.
|
|
183
|
+
*
|
|
184
|
+
* @param acl The code-defined ACL to store.
|
|
185
|
+
* @returns The user-editable ACL, or `null` when RBAC is disabled or no ACL was given.
|
|
128
186
|
*/
|
|
129
187
|
saveDefaultACL(acl: AccessControlList): Promise<AccessControlList | null>;
|
|
130
188
|
/**
|
|
@@ -138,8 +196,11 @@ export declare class ACLUtils {
|
|
|
138
196
|
* would issue overlapping commands against one session, which drivers reject or misorder.
|
|
139
197
|
*
|
|
140
198
|
* @param uids The unique identifiers of the ACLs to remove.
|
|
199
|
+
* @param options Set `unlessProtected: true` to never remove a code-defined ACL (see `removeACL()`).
|
|
141
200
|
*/
|
|
142
|
-
removeACLs(uids: string[]
|
|
201
|
+
removeACLs(uids: string[], options?: {
|
|
202
|
+
unlessProtected?: boolean;
|
|
203
|
+
}): Promise<AccessControlList[]>;
|
|
143
204
|
/**
|
|
144
205
|
* Atomic save of multiple ACLs in a single `acl`-scoped transaction (see `Transactional`). Used to restore
|
|
145
206
|
* a batch of ACL snapshots — e.g. by a `registerRollbackHook()` compensating action — if the entity-side
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rapidrest/service-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Provides all core functionality for RapidREST based backend services.",
|
|
5
5
|
"repository": "https://github.com/rapidrest/service-core.git",
|
|
6
6
|
"author": "RapidREST <rapidrests@gmail.com>",
|