From d4cb1bab8ec46adf5e05b8a9f2693aa7b6c7d839 Mon Sep 17 00:00:00 2001 From: Dimi Kot Date: Sun, 4 Jan 2026 18:56:21 -0800 Subject: [PATCH] v2.25.1: Add clientConnectedLogger; fix privacy cache issue when dealing with Ent slices; add VC#withoutFlavor() --- docs/classes/Cluster.md | 18 +- docs/classes/IDsCache.md | 17 +- docs/classes/IDsCacheCanReadIncomingEdge.md | 15 +- docs/classes/IDsCacheDeletable.md | 15 +- docs/classes/IDsCacheReadable.md | 15 +- docs/classes/IDsCacheUpdatable.md | 15 +- docs/classes/PgClient.md | 4 +- docs/classes/PgSchema.md | 3 +- docs/classes/Schema.md | 27 ++- docs/classes/TimelineManager.md | 2 +- docs/classes/VC.md | 53 +++-- docs/globals.md | 1 + docs/interfaces/ClientConnectedLoggerProps.md | 21 ++ docs/interfaces/ClientEndLoggerProps.md | 2 +- docs/interfaces/ClientQueryLoggerProps.md | 2 +- docs/interfaces/Loggers.md | 1 + docs/interfaces/RunOnShardErrorLoggerProps.md | 2 +- docs/interfaces/SchemaClass.md | 4 +- docs/interfaces/SwallowedErrorLoggerProps.md | 2 +- eslint.base.config.mjs | 1 + jest.config.base.js | 2 +- package-lock.json | 4 +- package.json | 4 +- src/abstract/Client.ts | 2 +- src/abstract/Cluster.ts | 4 + src/abstract/Loggers.ts | 6 + src/abstract/Schema.ts | 11 -- src/abstract/ShardIsNotDiscoverableError.ts | 6 +- src/abstract/TimelineManager.ts | 4 +- src/ent/IDsCache.ts | 19 +- src/ent/VC.ts | 35 ++++ src/ent/__tests__/VC.test.ts | 52 +++++ src/ent/mixins/PrimitiveMixin.ts | 12 +- src/ent/predicates/CanDeleteOutgoingEdge.ts | 6 +- src/ent/predicates/CanReadOutgoingEdge.ts | 6 +- src/ent/predicates/CanUpdateOutgoingEdge.ts | 6 +- .../predicates/IncomingEdgeFromVCExists.ts | 4 +- .../__tests__/CanReadOutgoingEdge.test.ts | 182 ++++++++++++++++++ src/pg/PgClient.ts | 17 +- .../PgSchema.islands-reconfig.test.ts | 5 + src/pg/__tests__/test-utils.ts | 1 + 41 files changed, 478 insertions(+), 130 deletions(-) create mode 100644 docs/interfaces/ClientConnectedLoggerProps.md create mode 100644 src/ent/predicates/__tests__/CanReadOutgoingEdge.test.ts diff --git a/docs/classes/Cluster.md b/docs/classes/Cluster.md index 3d4f81b..d651cea 100644 --- a/docs/classes/Cluster.md +++ b/docs/classes/Cluster.md @@ -57,7 +57,7 @@ queries (also, no implicit prewarming). > **prewarm**(`randomizedDelayMs`, `onInitialPrewarm`?): `void` -Defined in: [src/abstract/Cluster.ts:299](https://github.com/clickup/ent-framework/blob/master/src/abstract/Cluster.ts#L299) +Defined in: [src/abstract/Cluster.ts:303](https://github.com/clickup/ent-framework/blob/master/src/abstract/Cluster.ts#L303) Signals the Cluster to keep the Clients pre-warmed, e.g. open. (It's up to the particular Client's implementation, what does a "pre-warmed Client" @@ -87,7 +87,7 @@ pgbouncer or when DB is accessed over SSL). > **globalShard**(): [`Shard`](Shard.md)\<`TClient`\> -Defined in: [src/abstract/Cluster.ts:330](https://github.com/clickup/ent-framework/blob/master/src/abstract/Cluster.ts#L330) +Defined in: [src/abstract/Cluster.ts:334](https://github.com/clickup/ent-framework/blob/master/src/abstract/Cluster.ts#L334) Returns a global Shard of the Cluster. This method is made synchronous intentionally, to defer the I/O and possible errors to the moment of the @@ -103,7 +103,7 @@ actual query. > **nonGlobalShards**(): `Promise`\[]\> -Defined in: [src/abstract/Cluster.ts:337](https://github.com/clickup/ent-framework/blob/master/src/abstract/Cluster.ts#L337) +Defined in: [src/abstract/Cluster.ts:341](https://github.com/clickup/ent-framework/blob/master/src/abstract/Cluster.ts#L341) Returns all currently known (discovered) non-global Shards in the Cluster. @@ -117,7 +117,7 @@ Returns all currently known (discovered) non-global Shards in the Cluster. > **shard**(`id`): [`Shard`](Shard.md)\<`TClient`\> -Defined in: [src/abstract/Cluster.ts:357](https://github.com/clickup/ent-framework/blob/master/src/abstract/Cluster.ts#L357) +Defined in: [src/abstract/Cluster.ts:361](https://github.com/clickup/ent-framework/blob/master/src/abstract/Cluster.ts#L361) Returns Shard of a particular id. This method is made synchronous intentionally, to defer the I/O and possible errors to the moment of the @@ -149,7 +149,7 @@ the query), no matter whether it was an immediate call or a deferred one. > **shardByNo**(`shardNo`): [`Shard`](Shard.md)\<`TClient`\> -Defined in: [src/abstract/Cluster.ts:372](https://github.com/clickup/ent-framework/blob/master/src/abstract/Cluster.ts#L372) +Defined in: [src/abstract/Cluster.ts:376](https://github.com/clickup/ent-framework/blob/master/src/abstract/Cluster.ts#L376) Returns a Shard if we know its number. The idea: for each Shard number (even for non-discovered yet Shards), we keep the corresponding Shard @@ -174,7 +174,7 @@ Shard hasn't been discovered actually). > **randomShard**(`seed`?): `Promise`\<[`Shard`](Shard.md)\<`TClient`\>\> -Defined in: [src/abstract/Cluster.ts:380](https://github.com/clickup/ent-framework/blob/master/src/abstract/Cluster.ts#L380) +Defined in: [src/abstract/Cluster.ts:384](https://github.com/clickup/ent-framework/blob/master/src/abstract/Cluster.ts#L384) Returns a random Shard among the ones which are currently known (discovered) in the Cluster. @@ -195,7 +195,7 @@ Returns a random Shard among the ones which are currently known > **island**(`islandNo`): `Promise`\<[`Island`](Island.md)\<`TClient`\>\> -Defined in: [src/abstract/Cluster.ts:404](https://github.com/clickup/ent-framework/blob/master/src/abstract/Cluster.ts#L404) +Defined in: [src/abstract/Cluster.ts:408](https://github.com/clickup/ent-framework/blob/master/src/abstract/Cluster.ts#L408) Returns an Island by its number. @@ -215,7 +215,7 @@ Returns an Island by its number. > **islands**(): `Promise`\<[`Island`](Island.md)\<`TClient`\>[]\> -Defined in: [src/abstract/Cluster.ts:415](https://github.com/clickup/ent-framework/blob/master/src/abstract/Cluster.ts#L415) +Defined in: [src/abstract/Cluster.ts:419](https://github.com/clickup/ent-framework/blob/master/src/abstract/Cluster.ts#L419) Returns all Islands in the Cluster. @@ -229,7 +229,7 @@ Returns all Islands in the Cluster. > **rediscover**(`what`?): `Promise`\<`void`\> -Defined in: [src/abstract/Cluster.ts:425](https://github.com/clickup/ent-framework/blob/master/src/abstract/Cluster.ts#L425) +Defined in: [src/abstract/Cluster.ts:429](https://github.com/clickup/ent-framework/blob/master/src/abstract/Cluster.ts#L429) Triggers shards rediscovery and finishes as soon as it's done. To be used in unit tests mostly, because in real life, it's enough to just modify the diff --git a/docs/classes/IDsCache.md b/docs/classes/IDsCache.md index 0e8bc31..ddb8457 100644 --- a/docs/classes/IDsCache.md +++ b/docs/classes/IDsCache.md @@ -6,7 +6,7 @@ # Class: `abstract` IDsCache -Defined in: [src/ent/IDsCache.ts:3](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L3) +Defined in: [src/ent/IDsCache.ts:10](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L10) ## Extended by @@ -29,14 +29,15 @@ Defined in: [src/ent/IDsCache.ts:3](https://github.com/clickup/ent-framework/blo ### has() -> **has**(`id`): `boolean` +> **has**(`Ent`, `id`): `boolean` -Defined in: [src/ent/IDsCache.ts:6](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L6) +Defined in: [src/ent/IDsCache.ts:13](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L13) #### Parameters | Parameter | Type | | ------ | ------ | +| `Ent` | `EntClassAlike` | | `id` | `string` | #### Returns @@ -47,14 +48,15 @@ Defined in: [src/ent/IDsCache.ts:6](https://github.com/clickup/ent-framework/blo ### add() -> **add**(`id`, `value`): `void` +> **add**(`Ent`, `id`, `value`): `void` -Defined in: [src/ent/IDsCache.ts:10](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L10) +Defined in: [src/ent/IDsCache.ts:17](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L17) #### Parameters | Parameter | Type | Default value | | ------ | ------ | ------ | +| `Ent` | `EntClassAlike` | `undefined` | | `id` | `string` | `undefined` | | `value` | `boolean` | `true` | @@ -66,14 +68,15 @@ Defined in: [src/ent/IDsCache.ts:10](https://github.com/clickup/ent-framework/bl ### get() -> **get**(`id`): `undefined` \| `boolean` +> **get**(`Ent`, `id`): `undefined` \| `boolean` -Defined in: [src/ent/IDsCache.ts:14](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L14) +Defined in: [src/ent/IDsCache.ts:21](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L21) #### Parameters | Parameter | Type | | ------ | ------ | +| `Ent` | `EntClassAlike` | | `id` | `string` | #### Returns diff --git a/docs/classes/IDsCacheCanReadIncomingEdge.md b/docs/classes/IDsCacheCanReadIncomingEdge.md index 959c501..fc6489c 100644 --- a/docs/classes/IDsCacheCanReadIncomingEdge.md +++ b/docs/classes/IDsCacheCanReadIncomingEdge.md @@ -30,14 +30,15 @@ Defined in: [src/ent/predicates/Predicate.ts:49](https://github.com/clickup/ent- ### has() -> **has**(`id`): `boolean` +> **has**(`Ent`, `id`): `boolean` -Defined in: [src/ent/IDsCache.ts:6](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L6) +Defined in: [src/ent/IDsCache.ts:13](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L13) #### Parameters | Parameter | Type | | ------ | ------ | +| `Ent` | `EntClassAlike` | | `id` | `string` | #### Returns @@ -52,14 +53,15 @@ Defined in: [src/ent/IDsCache.ts:6](https://github.com/clickup/ent-framework/blo ### add() -> **add**(`id`, `value`): `void` +> **add**(`Ent`, `id`, `value`): `void` -Defined in: [src/ent/IDsCache.ts:10](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L10) +Defined in: [src/ent/IDsCache.ts:17](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L17) #### Parameters | Parameter | Type | Default value | | ------ | ------ | ------ | +| `Ent` | `EntClassAlike` | `undefined` | | `id` | `string` | `undefined` | | `value` | `boolean` | `true` | @@ -75,14 +77,15 @@ Defined in: [src/ent/IDsCache.ts:10](https://github.com/clickup/ent-framework/bl ### get() -> **get**(`id`): `undefined` \| `boolean` +> **get**(`Ent`, `id`): `undefined` \| `boolean` -Defined in: [src/ent/IDsCache.ts:14](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L14) +Defined in: [src/ent/IDsCache.ts:21](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L21) #### Parameters | Parameter | Type | | ------ | ------ | +| `Ent` | `EntClassAlike` | | `id` | `string` | #### Returns diff --git a/docs/classes/IDsCacheDeletable.md b/docs/classes/IDsCacheDeletable.md index 95822ec..f2bde37 100644 --- a/docs/classes/IDsCacheDeletable.md +++ b/docs/classes/IDsCacheDeletable.md @@ -30,14 +30,15 @@ Defined in: [src/ent/predicates/Predicate.ts:48](https://github.com/clickup/ent- ### has() -> **has**(`id`): `boolean` +> **has**(`Ent`, `id`): `boolean` -Defined in: [src/ent/IDsCache.ts:6](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L6) +Defined in: [src/ent/IDsCache.ts:13](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L13) #### Parameters | Parameter | Type | | ------ | ------ | +| `Ent` | `EntClassAlike` | | `id` | `string` | #### Returns @@ -52,14 +53,15 @@ Defined in: [src/ent/IDsCache.ts:6](https://github.com/clickup/ent-framework/blo ### add() -> **add**(`id`, `value`): `void` +> **add**(`Ent`, `id`, `value`): `void` -Defined in: [src/ent/IDsCache.ts:10](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L10) +Defined in: [src/ent/IDsCache.ts:17](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L17) #### Parameters | Parameter | Type | Default value | | ------ | ------ | ------ | +| `Ent` | `EntClassAlike` | `undefined` | | `id` | `string` | `undefined` | | `value` | `boolean` | `true` | @@ -75,14 +77,15 @@ Defined in: [src/ent/IDsCache.ts:10](https://github.com/clickup/ent-framework/bl ### get() -> **get**(`id`): `undefined` \| `boolean` +> **get**(`Ent`, `id`): `undefined` \| `boolean` -Defined in: [src/ent/IDsCache.ts:14](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L14) +Defined in: [src/ent/IDsCache.ts:21](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L21) #### Parameters | Parameter | Type | | ------ | ------ | +| `Ent` | `EntClassAlike` | | `id` | `string` | #### Returns diff --git a/docs/classes/IDsCacheReadable.md b/docs/classes/IDsCacheReadable.md index de7cea9..0fa5c1a 100644 --- a/docs/classes/IDsCacheReadable.md +++ b/docs/classes/IDsCacheReadable.md @@ -30,14 +30,15 @@ Defined in: [src/ent/predicates/Predicate.ts:46](https://github.com/clickup/ent- ### has() -> **has**(`id`): `boolean` +> **has**(`Ent`, `id`): `boolean` -Defined in: [src/ent/IDsCache.ts:6](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L6) +Defined in: [src/ent/IDsCache.ts:13](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L13) #### Parameters | Parameter | Type | | ------ | ------ | +| `Ent` | `EntClassAlike` | | `id` | `string` | #### Returns @@ -52,14 +53,15 @@ Defined in: [src/ent/IDsCache.ts:6](https://github.com/clickup/ent-framework/blo ### add() -> **add**(`id`, `value`): `void` +> **add**(`Ent`, `id`, `value`): `void` -Defined in: [src/ent/IDsCache.ts:10](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L10) +Defined in: [src/ent/IDsCache.ts:17](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L17) #### Parameters | Parameter | Type | Default value | | ------ | ------ | ------ | +| `Ent` | `EntClassAlike` | `undefined` | | `id` | `string` | `undefined` | | `value` | `boolean` | `true` | @@ -75,14 +77,15 @@ Defined in: [src/ent/IDsCache.ts:10](https://github.com/clickup/ent-framework/bl ### get() -> **get**(`id`): `undefined` \| `boolean` +> **get**(`Ent`, `id`): `undefined` \| `boolean` -Defined in: [src/ent/IDsCache.ts:14](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L14) +Defined in: [src/ent/IDsCache.ts:21](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L21) #### Parameters | Parameter | Type | | ------ | ------ | +| `Ent` | `EntClassAlike` | | `id` | `string` | #### Returns diff --git a/docs/classes/IDsCacheUpdatable.md b/docs/classes/IDsCacheUpdatable.md index f1ecd05..1df00bf 100644 --- a/docs/classes/IDsCacheUpdatable.md +++ b/docs/classes/IDsCacheUpdatable.md @@ -30,14 +30,15 @@ Defined in: [src/ent/predicates/Predicate.ts:47](https://github.com/clickup/ent- ### has() -> **has**(`id`): `boolean` +> **has**(`Ent`, `id`): `boolean` -Defined in: [src/ent/IDsCache.ts:6](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L6) +Defined in: [src/ent/IDsCache.ts:13](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L13) #### Parameters | Parameter | Type | | ------ | ------ | +| `Ent` | `EntClassAlike` | | `id` | `string` | #### Returns @@ -52,14 +53,15 @@ Defined in: [src/ent/IDsCache.ts:6](https://github.com/clickup/ent-framework/blo ### add() -> **add**(`id`, `value`): `void` +> **add**(`Ent`, `id`, `value`): `void` -Defined in: [src/ent/IDsCache.ts:10](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L10) +Defined in: [src/ent/IDsCache.ts:17](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L17) #### Parameters | Parameter | Type | Default value | | ------ | ------ | ------ | +| `Ent` | `EntClassAlike` | `undefined` | | `id` | `string` | `undefined` | | `value` | `boolean` | `true` | @@ -75,14 +77,15 @@ Defined in: [src/ent/IDsCache.ts:10](https://github.com/clickup/ent-framework/bl ### get() -> **get**(`id`): `undefined` \| `boolean` +> **get**(`Ent`, `id`): `undefined` \| `boolean` -Defined in: [src/ent/IDsCache.ts:14](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L14) +Defined in: [src/ent/IDsCache.ts:21](https://github.com/clickup/ent-framework/blob/master/src/ent/IDsCache.ts#L21) #### Parameters | Parameter | Type | | ------ | ------ | +| `Ent` | `EntClassAlike` | | `id` | `string` | #### Returns diff --git a/docs/classes/PgClient.md b/docs/classes/PgClient.md index 34242c5..054c6fb 100644 --- a/docs/classes/PgClient.md +++ b/docs/classes/PgClient.md @@ -364,7 +364,7 @@ database-agnostic Client API. > **acquireConn**(`subPoolConfig`?): `Promise`\<[`PgClientConn`](../interfaces/PgClientConn.md)\<`TPool`\>\> -Defined in: [src/pg/PgClient.ts:489](https://github.com/clickup/ent-framework/blob/master/src/pg/PgClient.ts#L489) +Defined in: [src/pg/PgClient.ts:492](https://github.com/clickup/ent-framework/blob/master/src/pg/PgClient.ts#L492) Called when the Client needs a connection in the default pool (when subPoolConfig is not passed), or in a sub-pool (see pool() method) to run a @@ -392,7 +392,7 @@ database-agnostic Client API. > **query**\<`TRow`\>(`__namedParameters`): `Promise`\<`TRow`[]\> -Defined in: [src/pg/PgClient.ts:513](https://github.com/clickup/ent-framework/blob/master/src/pg/PgClient.ts#L513) +Defined in: [src/pg/PgClient.ts:516](https://github.com/clickup/ent-framework/blob/master/src/pg/PgClient.ts#L516) Sends a query (internally, a multi-query) through the default Pool (if subPoolConfig is not passed), or through a named sub-pool (see pool() diff --git a/docs/classes/PgSchema.md b/docs/classes/PgSchema.md index 5556eac..0deaada 100644 --- a/docs/classes/PgSchema.md +++ b/docs/classes/PgSchema.md @@ -35,7 +35,7 @@ too limited in the queries the DB engine can execute. > **new PgSchema**\<`TTable`, `TUniqueKey`\>(`name`, `table`, `uniqueKey`): [`PgSchema`](PgSchema.md)\<`TTable`, `TUniqueKey`\> -Defined in: [src/abstract/Schema.ts:119](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L119) +Defined in: [src/abstract/Schema.ts:116](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L116) Used in e.g. inverses. This casts this.constructor to SchemaClass with all static methods and `new` semantic (TS doesn't do it by default; for TS, @@ -61,7 +61,6 @@ x.constructor is Function). | Property | Type | Description | | ------ | ------ | ------ | -| `hash` | `string` | - | | `constructor` | [`SchemaClass`](../interfaces/SchemaClass.md) | Used in e.g. inverses. This casts this.constructor to SchemaClass with all static methods and `new` semantic (TS doesn't do it by default; for TS, x.constructor is Function). | | `name` | `string` | For relational databases, it's likely a table name. | | `table` | `TTable` | Structure of the table. | diff --git a/docs/classes/Schema.md b/docs/classes/Schema.md index a79bf05..660e5b1 100644 --- a/docs/classes/Schema.md +++ b/docs/classes/Schema.md @@ -6,7 +6,7 @@ # Class: `abstract` Schema\ -Defined in: [src/abstract/Schema.ts:38](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L38) +Defined in: [src/abstract/Schema.ts:37](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L37) Schema is like a "table" in some database (sharded, but it's beyond the scope of Schema). It is also a factory of Query: it knows how to build runnable @@ -35,7 +35,7 @@ too limited in the queries the DB engine can execute. > **new Schema**\<`TTable`, `TUniqueKey`\>(`name`, `table`, `uniqueKey`): [`Schema`](Schema.md)\<`TTable`, `TUniqueKey`\> -Defined in: [src/abstract/Schema.ts:119](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L119) +Defined in: [src/abstract/Schema.ts:116](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L116) #### Parameters @@ -53,7 +53,6 @@ Defined in: [src/abstract/Schema.ts:119](https://github.com/clickup/ent-framewor | Property | Type | Description | | ------ | ------ | ------ | -| `hash` | `string` | - | | `constructor` | [`SchemaClass`](../interfaces/SchemaClass.md) | Used in e.g. inverses. This casts this.constructor to SchemaClass with all static methods and `new` semantic (TS doesn't do it by default; for TS, x.constructor is Function). | | `name` | `string` | For relational databases, it's likely a table name. | | `table` | `TTable` | Structure of the table. | @@ -65,7 +64,7 @@ Defined in: [src/abstract/Schema.ts:119](https://github.com/clickup/ent-framewor > `abstract` **idGen**(): [`Query`](../interfaces/Query.md)\<`string`\> -Defined in: [src/abstract/Schema.ts:57](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L57) +Defined in: [src/abstract/Schema.ts:54](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L54) Generates a new ID for the row. Used when e.g. there is a beforeInsert trigger on the Ent which needs to know the ID beforehand. @@ -80,7 +79,7 @@ trigger on the Ent which needs to know the ID beforehand. > `abstract` **insert**(`input`): [`Query`](../interfaces/Query.md)\<`null` \| `string`\> -Defined in: [src/abstract/Schema.ts:63](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L63) +Defined in: [src/abstract/Schema.ts:60](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L60) Creates a new row. Returns null if the row violates some unique key constraint, otherwise returns the row ID. @@ -101,7 +100,7 @@ constraint, otherwise returns the row ID. > `abstract` **upsert**(`input`): [`Query`](../interfaces/Query.md)\<`string`\> -Defined in: [src/abstract/Schema.ts:68](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L68) +Defined in: [src/abstract/Schema.ts:65](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L65) Upserts a row. Always returns the row ID. @@ -121,7 +120,7 @@ Upserts a row. Always returns the row ID. > `abstract` **update**(`id`, `input`): [`Query`](../interfaces/Query.md)\<`boolean`\> -Defined in: [src/abstract/Schema.ts:73](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L73) +Defined in: [src/abstract/Schema.ts:70](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L70) Updates one single row by its ID. Returns true if it actually existed. @@ -142,7 +141,7 @@ Updates one single row by its ID. Returns true if it actually existed. > `abstract` **delete**(`id`): [`Query`](../interfaces/Query.md)\<`boolean`\> -Defined in: [src/abstract/Schema.ts:78](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L78) +Defined in: [src/abstract/Schema.ts:75](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L75) Deletes a row by id. Returns true if it actually existed. @@ -162,7 +161,7 @@ Deletes a row by id. Returns true if it actually existed. > `abstract` **load**(`id`): [`Query`](../interfaces/Query.md)\<`null` \| [`Row`](../type-aliases/Row.md)\<`TTable`\>\> -Defined in: [src/abstract/Schema.ts:84](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L84) +Defined in: [src/abstract/Schema.ts:81](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L81) "Load" family of methods means that we load exactly one row. This one returns a row by its ID or null if it's not found. @@ -183,7 +182,7 @@ returns a row by its ID or null if it's not found. > `abstract` **loadBy**(`input`): [`Query`](../interfaces/Query.md)\<`null` \| [`Row`](../type-aliases/Row.md)\<`TTable`\>\> -Defined in: [src/abstract/Schema.ts:90](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L90) +Defined in: [src/abstract/Schema.ts:87](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L87) Loads one single row by its unique key ("by" denotes that it's based on an unique key, not on an ID). Returns null if it's not found. @@ -204,7 +203,7 @@ unique key, not on an ID). Returns null if it's not found. > `abstract` **selectBy**(`input`): [`Query`](../interfaces/Query.md)\<[`Row`](../type-aliases/Row.md)\<`TTable`\>[]\> -Defined in: [src/abstract/Schema.ts:99](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L99) +Defined in: [src/abstract/Schema.ts:96](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L96) "Select" family of methods means that we load multiple rows ("by" denotes that it's based on an unique key, not on an arbitrary query). This one @@ -226,7 +225,7 @@ returns all rows whose unique key prefix matches the input. > `abstract` **select**(`input`): [`Query`](../interfaces/Query.md)\<[`Row`](../type-aliases/Row.md)\<`TTable`\>[]\> -Defined in: [src/abstract/Schema.ts:106](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L106) +Defined in: [src/abstract/Schema.ts:103](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L103) Returns all rows matching an arbitrary query. @@ -246,7 +245,7 @@ Returns all rows matching an arbitrary query. > `abstract` **count**(`input`): [`Query`](../interfaces/Query.md)\<`number`\> -Defined in: [src/abstract/Schema.ts:111](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L111) +Defined in: [src/abstract/Schema.ts:108](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L108) Returns the number of rows matching an arbitrary query. @@ -266,7 +265,7 @@ Returns the number of rows matching an arbitrary query. > `abstract` **exists**(`input`): [`Query`](../interfaces/Query.md)\<`boolean`\> -Defined in: [src/abstract/Schema.ts:117](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L117) +Defined in: [src/abstract/Schema.ts:114](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L114) An optimized version of count() for the cases where we only need to know whether at least one row exists, and don't need a precise count. diff --git a/docs/classes/TimelineManager.md b/docs/classes/TimelineManager.md index f75c4d8..d7f0efc 100644 --- a/docs/classes/TimelineManager.md +++ b/docs/classes/TimelineManager.md @@ -28,7 +28,7 @@ Defined in: [src/abstract/TimelineManager.ts:15](https://github.com/clickup/ent- | ------ | ------ | ------ | | `maxLagMs` | `MaybeCallable`\<`number`\> | Time interval after which a replica is declared as "caught up" even if it's not caught up. This is to not read from master forever when something has happened with the replica. | | `refreshMs` | `MaybeCallable`\<`number`\> | Up to how often we call triggerRefresh(). | -| `triggerRefresh` | () => `Promise`\<`unknown`\> | This method is called time to time to refresh the data which is later returned by currentPos(). Makes sense for replica connections which execute queries rarely: for them, the framework triggers the update when the fresh data is needed. | +| `triggerRefresh` | () => `Promise`\<`unknown`\> | This method is called from time to time to refresh the data which is later returned by currentPos(). Makes sense for replica connections which execute queries rarely: for them, the framework triggers the update when the fresh data is needed. | #### Returns diff --git a/docs/classes/VC.md b/docs/classes/VC.md index dd8e27b..a5a10af 100644 --- a/docs/classes/VC.md +++ b/docs/classes/VC.md @@ -241,7 +241,7 @@ Returns a new VC derived from the current one, but with empty cache. > **withTransitiveMasterFreshness**(): [`VC`](VC.md) -Defined in: [src/ent/VC.ts:234](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L234) +Defined in: [src/ent/VC.ts:235](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L235) Returns a new VC derived from the current one, but with master freshness. Master freshness is inherited by ent.vc after an Ent is loaded. @@ -256,7 +256,7 @@ Master freshness is inherited by ent.vc after an Ent is loaded. > **withOneTimeStaleReplica**(): [`VC`](VC.md) -Defined in: [src/ent/VC.ts:261](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L261) +Defined in: [src/ent/VC.ts:262](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L262) Returns a new VC derived from the current one, but which forces an Ent to be loaded always from replica. Freshness is NOT inherited by Ents (not @@ -277,7 +277,7 @@ the master. > **withDefaultFreshness**(): [`VC`](VC.md) -Defined in: [src/ent/VC.ts:284](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L284) +Defined in: [src/ent/VC.ts:285](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L285) Creates a new VC with default freshness (i.e. not sticky to master or replica, auto-detected on request). Generally, it's not a good idea to use @@ -296,7 +296,7 @@ history of the VC, but for e.g. tests or benchmarks, it's fine. > **withFlavor**(`prepend`, ...`flavors`): `this` -Defined in: [src/ent/VC.ts:305](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L305) +Defined in: [src/ent/VC.ts:306](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L306) Returns a new VC derived from the current one adding some more flavors to it. If no flavors were added, returns the same VC (`this`). @@ -316,7 +316,7 @@ it. If no flavors were added, returns the same VC (`this`). > **withFlavor**(...`flavors`): `this` -Defined in: [src/ent/VC.ts:306](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L306) +Defined in: [src/ent/VC.ts:307](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L307) Returns a new VC derived from the current one adding some more flavors to it. If no flavors were added, returns the same VC (`this`). @@ -333,11 +333,32 @@ it. If no flavors were added, returns the same VC (`this`). *** +### withoutFlavor() + +> **withoutFlavor**(...`flavorClasses`): `this` + +Defined in: [src/ent/VC.ts:345](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L345) + +Returns a new VC derived from the current one removing the specified flavors. +If no flavors were removed, returns the same VC (`this`). + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| ...`flavorClasses` | (...`args`) => [`VCFlavor`](VCFlavor.md)[] | + +#### Returns + +`this` + +*** + ### withNewTrace() > **withNewTrace**(`trace`): [`VC`](VC.md) -Defined in: [src/ent/VC.ts:343](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L343) +Defined in: [src/ent/VC.ts:378](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L378) Derives the VC with new trace ID. @@ -357,7 +378,7 @@ Derives the VC with new trace ID. > **withHeartbeater**(`heartbeater`): [`VC`](VC.md) -Defined in: [src/ent/VC.ts:359](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L359) +Defined in: [src/ent/VC.ts:394](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L394) Derives the VC with the provided heartbeater injected. @@ -379,7 +400,7 @@ Derives the VC with the provided heartbeater injected. > **toOmniDangerous**(): [`VC`](VC.md) -Defined in: [src/ent/VC.ts:379](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L379) +Defined in: [src/ent/VC.ts:414](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L414) Creates a new VC upgraded to omni permissions. This VC will not be placed to some Ent's ent.vc property; instead, it will be @@ -396,7 +417,7 @@ to a guest VC (see Ent.ts). > **toGuest**(): [`VC`](VC.md) -Defined in: [src/ent/VC.ts:396](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L396) +Defined in: [src/ent/VC.ts:431](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L431) Creates a new VC downgraded to guest permissions. @@ -410,7 +431,7 @@ Creates a new VC downgraded to guest permissions. > **isOmni**(): `boolean` -Defined in: [src/ent/VC.ts:411](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L411) +Defined in: [src/ent/VC.ts:446](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L446) Checks if it's an omni VC. @@ -424,7 +445,7 @@ Checks if it's an omni VC. > **isGuest**(): `boolean` -Defined in: [src/ent/VC.ts:418](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L418) +Defined in: [src/ent/VC.ts:453](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L453) Checks if it's a guest VC. @@ -438,7 +459,7 @@ Checks if it's a guest VC. > **isLoggedIn**(): `boolean` -Defined in: [src/ent/VC.ts:425](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L425) +Defined in: [src/ent/VC.ts:460](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L460) Checks if it's a regular user (i.e. owning) VC. @@ -452,7 +473,7 @@ Checks if it's a regular user (i.e. owning) VC. > **flavor**\<`TFlavor`\>(`flavor`): `null` \| `TFlavor` -Defined in: [src/ent/VC.ts:432](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L432) +Defined in: [src/ent/VC.ts:467](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L467) Returns VC's flavor of the particular type. @@ -478,7 +499,7 @@ Returns VC's flavor of the particular type. > **toString**(`withInstanceNumber`): `string` -Defined in: [src/ent/VC.ts:441](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L441) +Defined in: [src/ent/VC.ts:476](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L476) Used for debugging purposes. @@ -498,7 +519,7 @@ Used for debugging purposes. > **toAnnotation**(): [`QueryAnnotation`](../interfaces/QueryAnnotation.md) -Defined in: [src/ent/VC.ts:460](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L460) +Defined in: [src/ent/VC.ts:495](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L495) Returns a debug annotation of this VC. @@ -512,7 +533,7 @@ Returns a debug annotation of this VC. > **toLowerInternal**(`principal`): [`VC`](VC.md) -Defined in: [src/ent/VC.ts:491](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L491) +Defined in: [src/ent/VC.ts:526](https://github.com/clickup/ent-framework/blob/master/src/ent/VC.ts#L526) Used internally by Ent framework to lower permissions of an injected VC. For guest, principal === null. diff --git a/docs/globals.md b/docs/globals.md index 19e8b33..de39312 100644 --- a/docs/globals.md +++ b/docs/globals.md @@ -96,6 +96,7 @@ - [SwallowedErrorLoggerProps](interfaces/SwallowedErrorLoggerProps.md) - [RunOnShardErrorLoggerProps](interfaces/RunOnShardErrorLoggerProps.md) - [ClientEndLoggerProps](interfaces/ClientEndLoggerProps.md) +- [ClientConnectedLoggerProps](interfaces/ClientConnectedLoggerProps.md) - [Query](interfaces/Query.md) - [QueryAnnotation](interfaces/QueryAnnotation.md) - [SchemaClass](interfaces/SchemaClass.md) diff --git a/docs/interfaces/ClientConnectedLoggerProps.md b/docs/interfaces/ClientConnectedLoggerProps.md new file mode 100644 index 0000000..aa9b73a --- /dev/null +++ b/docs/interfaces/ClientConnectedLoggerProps.md @@ -0,0 +1,21 @@ +[**@clickup/ent-framework**](../README.md) + +*** + +[@clickup/ent-framework](../globals.md) / ClientConnectedLoggerProps + +# Interface: ClientConnectedLoggerProps\ + +Defined in: [src/abstract/Loggers.ts:78](https://github.com/clickup/ent-framework/blob/master/src/abstract/Loggers.ts#L78) + +## Type Parameters + +| Type Parameter | +| ------ | +| `TNode` | + +## Properties + +| Property | Type | +| ------ | ------ | +| `node` | `TNode` | diff --git a/docs/interfaces/ClientEndLoggerProps.md b/docs/interfaces/ClientEndLoggerProps.md index 0d860bb..89163db 100644 --- a/docs/interfaces/ClientEndLoggerProps.md +++ b/docs/interfaces/ClientEndLoggerProps.md @@ -6,7 +6,7 @@ # Interface: ClientEndLoggerProps\ -Defined in: [src/abstract/Loggers.ts:70](https://github.com/clickup/ent-framework/blob/master/src/abstract/Loggers.ts#L70) +Defined in: [src/abstract/Loggers.ts:72](https://github.com/clickup/ent-framework/blob/master/src/abstract/Loggers.ts#L72) ## Type Parameters diff --git a/docs/interfaces/ClientQueryLoggerProps.md b/docs/interfaces/ClientQueryLoggerProps.md index 589331c..82add90 100644 --- a/docs/interfaces/ClientQueryLoggerProps.md +++ b/docs/interfaces/ClientQueryLoggerProps.md @@ -6,7 +6,7 @@ # Interface: ClientQueryLoggerProps -Defined in: [src/abstract/Loggers.ts:25](https://github.com/clickup/ent-framework/blob/master/src/abstract/Loggers.ts#L25) +Defined in: [src/abstract/Loggers.ts:27](https://github.com/clickup/ent-framework/blob/master/src/abstract/Loggers.ts#L27) ## Properties diff --git a/docs/interfaces/Loggers.md b/docs/interfaces/Loggers.md index 6138293..c768e17 100644 --- a/docs/interfaces/Loggers.md +++ b/docs/interfaces/Loggers.md @@ -27,3 +27,4 @@ EventEmitter for several reasons: | `swallowedErrorLogger` | (`props`: [`SwallowedErrorLoggerProps`](SwallowedErrorLoggerProps.md)) => `void` | Logs errors which did not throw through (typically recoverable). | | `runOnShardErrorLogger?` | (`props`: [`RunOnShardErrorLoggerProps`](RunOnShardErrorLoggerProps.md)) => `void` | Called when Island-from-Shard location fails (e.g. no such Shard), or when a query on a particular Shard fails due to any reason (like transport error). Mostly used in unit tests, since it's called for every retry. | | `clientEndLogger?` | (`props`: [`ClientEndLoggerProps`](ClientEndLoggerProps.md)\<`TNode`\>) => `void` | Called when a Client gets ended due to dynamic Islands reconfiguration. Allows to debug flaky Island reconfiguration. | +| `clientConnectedLogger?` | (`props`: [`ClientConnectedLoggerProps`](ClientConnectedLoggerProps.md)\<`TNode`\>) => `void` | Called when a Client connects to the database. | diff --git a/docs/interfaces/RunOnShardErrorLoggerProps.md b/docs/interfaces/RunOnShardErrorLoggerProps.md index 1fd4336..dffa4f8 100644 --- a/docs/interfaces/RunOnShardErrorLoggerProps.md +++ b/docs/interfaces/RunOnShardErrorLoggerProps.md @@ -6,7 +6,7 @@ # Interface: RunOnShardErrorLoggerProps -Defined in: [src/abstract/Loggers.ts:65](https://github.com/clickup/ent-framework/blob/master/src/abstract/Loggers.ts#L65) +Defined in: [src/abstract/Loggers.ts:67](https://github.com/clickup/ent-framework/blob/master/src/abstract/Loggers.ts#L67) ## Properties diff --git a/docs/interfaces/SchemaClass.md b/docs/interfaces/SchemaClass.md index 032179a..9598234 100644 --- a/docs/interfaces/SchemaClass.md +++ b/docs/interfaces/SchemaClass.md @@ -6,7 +6,7 @@ # Interface: SchemaClass -Defined in: [src/abstract/Schema.ts:16](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L16) +Defined in: [src/abstract/Schema.ts:15](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L15) ## Constructors @@ -14,7 +14,7 @@ Defined in: [src/abstract/Schema.ts:16](https://github.com/clickup/ent-framework > **new SchemaClass**\<`TTable`, `TUniqueKey`\>(`name`, `table`, `uniqueKey`?): [`Schema`](../classes/Schema.md)\<`TTable`, `TUniqueKey`\> -Defined in: [src/abstract/Schema.ts:17](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L17) +Defined in: [src/abstract/Schema.ts:16](https://github.com/clickup/ent-framework/blob/master/src/abstract/Schema.ts#L16) #### Parameters diff --git a/docs/interfaces/SwallowedErrorLoggerProps.md b/docs/interfaces/SwallowedErrorLoggerProps.md index d2b8df4..1c3c245 100644 --- a/docs/interfaces/SwallowedErrorLoggerProps.md +++ b/docs/interfaces/SwallowedErrorLoggerProps.md @@ -6,7 +6,7 @@ # Interface: SwallowedErrorLoggerProps -Defined in: [src/abstract/Loggers.ts:58](https://github.com/clickup/ent-framework/blob/master/src/abstract/Loggers.ts#L58) +Defined in: [src/abstract/Loggers.ts:60](https://github.com/clickup/ent-framework/blob/master/src/abstract/Loggers.ts#L60) ## Properties diff --git a/eslint.base.config.mjs b/eslint.base.config.mjs index 934c813..4a334cb 100644 --- a/eslint.base.config.mjs +++ b/eslint.base.config.mjs @@ -692,6 +692,7 @@ export default function createConfig({ ], rules: { "local/zod-prefer-safe-nullish": "off", + "local/comment-punctuation": "off", // Otherwise, it adds a period after every multiline // commit line. }, }, ]; diff --git a/jest.config.base.js b/jest.config.base.js index cee966c..90408ec 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -8,6 +8,6 @@ module.exports = { ? {} : { forceExit: true, testTimeout: 30000 }), transform: { - "\\.ts$": "ts-jest", + "\\.ts$": ["ts-jest", { diagnostics: { ignoreCodes: [151002] } }], }, }; diff --git a/package-lock.json b/package-lock.json index fb13fca..e13b885 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@clickup/ent-framework", - "version": "2.24.2", + "version": "2.25.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@clickup/ent-framework", - "version": "2.24.2", + "version": "2.25.1", "license": "MIT", "dependencies": { "chalk": "^4.1.2", diff --git a/package.json b/package.json index 5194ef1..3e154d9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@clickup/ent-framework", "description": "A PostgreSQL graph-database-alike library with microsharding and row-level security", - "version": "2.24.2", + "version": "2.25.1", "license": "MIT", "keywords": [ "postgresql", @@ -46,6 +46,7 @@ "table": "^6.8.0" }, "devDependencies": { + "@eslint/js": "^9.35.0", "@stylistic/eslint-plugin": "^5.3.1", "@clickup/pg-id": "^2.16.1", "@types/jest": "^29.5.5", @@ -67,6 +68,7 @@ "eslint-plugin-typescript-enum": "^2.1.0", "eslint-plugin-unused-imports": "^4.2.0", "eslint": "^9.35.0", + "globals": "^17.0.0", "jest": "^29.7.0", "prettier": "3.2.1", "streaming-iterables": "^7.1.0", diff --git a/src/abstract/Client.ts b/src/abstract/Client.ts index 71ff9d3..4e324fa 100644 --- a/src/abstract/Client.ts +++ b/src/abstract/Client.ts @@ -184,7 +184,7 @@ export abstract class Client { */ @Memoize( (QueryClass, schema, additionalShape, _, disableBatching) => - `${objectId(QueryClass)}:${schema.hash}:${additionalShape}:${disableBatching}`, + `${objectId(QueryClass)}:${objectId(schema)}:${additionalShape}:${disableBatching}`, ) batcher( _QueryClass: Function, diff --git a/src/abstract/Cluster.ts b/src/abstract/Cluster.ts index b74986b..44be104 100644 --- a/src/abstract/Cluster.ts +++ b/src/abstract/Cluster.ts @@ -198,6 +198,10 @@ export class Cluster { this.options.loggers.clientQueryLogger?.(props); loggers.clientQueryLogger?.(props); }, + clientConnectedLogger: (props) => { + this.options.loggers.clientConnectedLogger?.(props); + loggers.clientConnectedLogger?.(props); + }, }; return client; }, diff --git a/src/abstract/Loggers.ts b/src/abstract/Loggers.ts index 065c81f..6618b0a 100644 --- a/src/abstract/Loggers.ts +++ b/src/abstract/Loggers.ts @@ -20,6 +20,8 @@ export interface Loggers { /** Called when a Client gets ended due to dynamic Islands reconfiguration. * Allows to debug flaky Island reconfiguration. */ clientEndLogger?: (props: ClientEndLoggerProps) => void; + /** Called when a Client connects to the database. */ + clientConnectedLogger?: (props: ClientConnectedLoggerProps) => void; } export interface ClientQueryLoggerProps { @@ -72,3 +74,7 @@ export interface ClientEndLoggerProps { key: string; node: TNode; } + +export interface ClientConnectedLoggerProps { + node: TNode; +} diff --git a/src/abstract/Schema.ts b/src/abstract/Schema.ts index 295470d..444a691 100644 --- a/src/abstract/Schema.ts +++ b/src/abstract/Schema.ts @@ -1,4 +1,3 @@ -import hash from "object-hash"; import type { CountInput, ExistsInput, @@ -39,8 +38,6 @@ export abstract class Schema< TTable extends Table, TUniqueKey extends UniqueKey = UniqueKey, > { - readonly hash: string; - /** * Used in e.g. inverses. This casts this.constructor to SchemaClass with all * static methods and `new` semantic (TS doesn't do it by default; for TS, @@ -147,13 +144,5 @@ export abstract class Schema< ); } } - - this.hash = - this.name + - ":" + - hash([this.table, this.uniqueKey], { - algorithm: "md5", - ignoreUnknown: true, - }); } } diff --git a/src/abstract/ShardIsNotDiscoverableError.ts b/src/abstract/ShardIsNotDiscoverableError.ts index 83b20fc..fd8d1d7 100644 --- a/src/abstract/ShardIsNotDiscoverableError.ts +++ b/src/abstract/ShardIsNotDiscoverableError.ts @@ -14,6 +14,10 @@ export class ShardIsNotDiscoverableError extends ShardError { islands: Array>, elapsed: number, ) { + const now = new Date().toLocaleTimeString("en-US", { + hour12: false, + timeZoneName: "short", + }); super( `Shard ${shardNo} is not discoverable: no such Shard in the Cluster? some Islands are down? connections limit?` + (errors.length > 0 @@ -33,7 +37,7 @@ export class ShardIsNotDiscoverableError extends ShardError { islands .map(({ no, clients }) => `${no}@${clients[0].options.name}`) .join(", ") + - `; cached discovery took ${elapsed} ms`, + `; cached discovery took ${elapsed} ms at ${now}`, ); } } diff --git a/src/abstract/TimelineManager.ts b/src/abstract/TimelineManager.ts index cbb11b0..634303f 100644 --- a/src/abstract/TimelineManager.ts +++ b/src/abstract/TimelineManager.ts @@ -19,8 +19,8 @@ export class TimelineManager { public readonly maxLagMs: MaybeCallable, /** Up to how often we call triggerRefresh(). */ private refreshMs: MaybeCallable, - /** This method is called time to time to refresh the data which is later - * returned by currentPos(). Makes sense for replica connections which + /** This method is called from time to time to refresh the data which is + * later returned by currentPos(). Makes sense for replica connections which * execute queries rarely: for them, the framework triggers the update when * the fresh data is needed. */ private triggerRefresh: () => Promise, diff --git a/src/ent/IDsCache.ts b/src/ent/IDsCache.ts index 263945b..3ceeee5 100644 --- a/src/ent/IDsCache.ts +++ b/src/ent/IDsCache.ts @@ -1,17 +1,24 @@ import QuickLRU from "quick-lru"; +import { objectId } from "../internal/objectId"; + +const SEP = ":"; + +interface EntClassAlike { + SCHEMA: object; +} export abstract class IDsCache { private ids = new QuickLRU({ maxSize: 2000 }); - has(id: string): boolean { - return this.ids.has(id); + has(Ent: EntClassAlike, id: string): boolean { + return this.ids.has(objectId(Ent) + SEP + id); } - add(id: string, value: boolean = true): void { - this.ids.set(id, value); + add(Ent: EntClassAlike, id: string, value: boolean = true): void { + this.ids.set(objectId(Ent) + SEP + id, value); } - get(id: string): boolean | undefined { - return this.ids.get(id); + get(Ent: EntClassAlike, id: string): boolean | undefined { + return this.ids.get(objectId(Ent) + SEP + id); } } diff --git a/src/ent/VC.ts b/src/ent/VC.ts index 2007a52..f1751ce 100644 --- a/src/ent/VC.ts +++ b/src/ent/VC.ts @@ -231,6 +231,7 @@ export class VC { * Returns a new VC derived from the current one, but with master freshness. * Master freshness is inherited by ent.vc after an Ent is loaded. */ + @Memoize() withTransitiveMasterFreshness(): VC { if (this.freshness === MASTER) { return this; @@ -337,6 +338,40 @@ export class VC { : this; } + /** + * Returns a new VC derived from the current one removing the specified flavors. + * If no flavors were removed, returns the same VC (`this`). + */ + withoutFlavor( + ...flavorClasses: Array VCFlavor> + ): this { + if (!flavorClasses.length) { + return this; + } + + const newFlavors = new Map(this.flavors); + let removed = false; + + for (const flavorClass of flavorClasses) { + if (newFlavors.delete(flavorClass)) { + removed = true; + } + } + + return removed + ? (new VC( + this.trace, + this.principal, + this.freshness, + this.timelines, + newFlavors, + this.heartbeater, + this.isRoot, + this.cachesExpirationMs, + ) as this) + : this; + } + /** * Derives the VC with new trace ID. */ diff --git a/src/ent/__tests__/VC.test.ts b/src/ent/__tests__/VC.test.ts index 3e099d2..2333c26 100644 --- a/src/ent/__tests__/VC.test.ts +++ b/src/ent/__tests__/VC.test.ts @@ -74,3 +74,55 @@ test("VC flavor prepend and append", () => { "vc:guest(VCTest2:tNew,VCTest1:some)", ); }); + +describe("VC.withoutFlavor", () => { + test("VC withoutFlavor removes single flavor", () => { + const vc = createVC() + .withFlavor(new VCTest1("test1")) + .withFlavor(new VCTest2("test2")); + const vcCopy = vc.withoutFlavor(VCTest1); + + expect(vcCopy.flavor(VCTest1)).toBeNull(); + expect(vcCopy.flavor(VCTest2)).toBeInstanceOf(VCTest2); + }); + + test("VC withoutFlavor removes multiple flavors", () => { + const vc = createVC() + .withFlavor(new VCTest1("test1")) + .withFlavor(new VCTest2("test2")); + const vcCopy = vc.withoutFlavor(VCTest1, VCTest2); + + expect(vcCopy.flavor(VCTest1)).toBeNull(); + expect(vcCopy.flavor(VCTest2)).toBeNull(); + }); + + test("VC withoutFlavor returns same instance when no flavors removed", () => { + const vc = createVC().withFlavor(new VCTest1("test1")); + const vcCopy = vc.withoutFlavor(VCTest2); + + expect(vcCopy).toBe(vc); + }); + + test("VC withoutFlavor returns same instance when no flavor classes provided", () => { + const vc = createVC().withFlavor(new VCTest1("test1")); + const vcCopy = vc.withoutFlavor(); + + expect(vcCopy).toBe(vc); + }); + + test("VC withoutFlavor handles non-existent flavor gracefully", () => { + const vc = createVC(); + const vcCopy = vc.withoutFlavor(VCTest1); + + expect(vcCopy).toBe(vc); + expect(vcCopy.flavor(VCTest1)).toBeNull(); + }); + + test("VC withoutFlavor should not mutate the original VC", () => { + const vc = createVC().withFlavor(new VCTest1("test1")); + const vcCopy = vc.withoutFlavor(VCTest1); + + expect(vcCopy).not.toBe(vc); + expect(vc.flavor(VCTest1)).toBeInstanceOf(VCTest1); + }); +}); diff --git a/src/ent/mixins/PrimitiveMixin.ts b/src/ent/mixins/PrimitiveMixin.ts index f34ba55..236b678 100644 --- a/src/ent/mixins/PrimitiveMixin.ts +++ b/src/ent/mixins/PrimitiveMixin.ts @@ -241,7 +241,7 @@ export function PrimitiveMixin< ), true, ]; - vc.cache(IDsCacheUpdatable).add(id2); // to enable privacy checks in beforeInsert triggers + vc.cache(IDsCacheUpdatable).add(this, id2); // to enable privacy checks in beforeInsert triggers // Inverses which we're going to create. const inverseRows = this.INVERSES.map((inverse) => ({ @@ -396,7 +396,7 @@ export function PrimitiveMixin< vc.timeline(shard, this.SCHEMA.name), vc.freshness, ); - vc.cache(IDsCacheUpdatable).add(id); + vc.cache(IDsCacheUpdatable).add(this, id); return id; } @@ -633,7 +633,7 @@ export function PrimitiveMixin< ); if (updated) { - this.vc.cache(IDsCacheUpdatable).add(this[ID]); + this.vc.cache(IDsCacheUpdatable).add(this.constructor, this[ID]); await mapJoin( this.constructor.INVERSES, async (inverse) => @@ -689,7 +689,7 @@ export function PrimitiveMixin< ); if (deleted) { - this.vc.cache(IDsCacheUpdatable).add(this[ID]); + this.vc.cache(IDsCacheUpdatable).add(this.constructor, this[ID]); await mapJoin(this.constructor.INVERSES, async (inverse) => inverse.afterDelete( this.vc, @@ -753,9 +753,9 @@ export function PrimitiveMixin< ); const ent = await creator(vc, this); - ent.vc.cache(IDsCacheReadable).add(ent[ID]); + ent.vc.cache(IDsCacheReadable).add(this, ent[ID]); if (vc !== ent.vc) { - vc.cache(IDsCacheReadable).add(ent[ID]); + vc.cache(IDsCacheReadable).add(this, ent[ID]); } return ent; diff --git a/src/ent/predicates/CanDeleteOutgoingEdge.ts b/src/ent/predicates/CanDeleteOutgoingEdge.ts index 237d2b1..72b81b1 100644 --- a/src/ent/predicates/CanDeleteOutgoingEdge.ts +++ b/src/ent/predicates/CanDeleteOutgoingEdge.ts @@ -26,12 +26,12 @@ export class CanDeleteOutgoingEdge } const cache = vc.cache(IDsCacheDeletable); - if (cache.has(toID!)) { + if (cache.has(this.toEntClass, toID)) { return true; } // Load the target Ent and check that it's deletable. - const toEnt = await this.toEntClass.loadNullable(vc, toID!); + const toEnt = await this.toEntClass.loadNullable(vc, toID); if (toEnt === null) { return true; } @@ -39,7 +39,7 @@ export class CanDeleteOutgoingEdge await this.toEntClass.VALIDATION.validateDelete(vc, toEnt); // Sill here and not thrown? save to the cache. - cache.add(toID!); + cache.add(this.toEntClass, toID); return true; } } diff --git a/src/ent/predicates/CanReadOutgoingEdge.ts b/src/ent/predicates/CanReadOutgoingEdge.ts index c42cf04..9bfc3cc 100644 --- a/src/ent/predicates/CanReadOutgoingEdge.ts +++ b/src/ent/predicates/CanReadOutgoingEdge.ts @@ -34,13 +34,13 @@ export class CanReadOutgoingEdge } const cache = vc.cache(IDsCacheReadable); - if (cache.has(toID!)) { + if (cache.has(this.toEntClass, toID)) { return true; } - await this.toEntClass.loadX(vc, toID!); + await this.toEntClass.loadX(vc, toID); // sill here and not thrown? save to the cache - cache.add(toID!); + cache.add(this.toEntClass, toID); return true; } } diff --git a/src/ent/predicates/CanUpdateOutgoingEdge.ts b/src/ent/predicates/CanUpdateOutgoingEdge.ts index ccc9dda..c1333d2 100644 --- a/src/ent/predicates/CanUpdateOutgoingEdge.ts +++ b/src/ent/predicates/CanUpdateOutgoingEdge.ts @@ -26,12 +26,12 @@ export class CanUpdateOutgoingEdge } const cache = vc.cache(IDsCacheUpdatable); - if (cache.has(toID!)) { + if (cache.has(this.toEntClass, toID)) { return true; } // load the target Ent and check that it's updatable - const toEnt = await this.toEntClass.loadX(vc, toID!); + const toEnt = await this.toEntClass.loadX(vc, toID); await this.toEntClass.VALIDATION.validateUpdate( vc, toEnt, @@ -40,7 +40,7 @@ export class CanUpdateOutgoingEdge ); // sill here and not thrown? save to the cache - cache.add(toID!); + cache.add(this.toEntClass, toID); return true; } } diff --git a/src/ent/predicates/IncomingEdgeFromVCExists.ts b/src/ent/predicates/IncomingEdgeFromVCExists.ts index 0c09f05..4049b25 100644 --- a/src/ent/predicates/IncomingEdgeFromVCExists.ts +++ b/src/ent/predicates/IncomingEdgeFromVCExists.ts @@ -46,7 +46,7 @@ export class IncomingEdgeFromVCExists async check(vc: VC, row: RowWithID): Promise { const cache = vc.cache(IDsCacheCanReadIncomingEdge); const cacheKey = nullthrows(row[ID]) + ":" + this.instanceID; - if (cache.has(cacheKey)) { + if (cache.has(this.EntEdge, cacheKey)) { return true; } @@ -76,7 +76,7 @@ export class IncomingEdgeFromVCExists } if (allow) { - cache.add(cacheKey); + cache.add(this.EntEdge, cacheKey); return true; } diff --git a/src/ent/predicates/__tests__/CanReadOutgoingEdge.test.ts b/src/ent/predicates/__tests__/CanReadOutgoingEdge.test.ts new file mode 100644 index 0000000..469b0e7 --- /dev/null +++ b/src/ent/predicates/__tests__/CanReadOutgoingEdge.test.ts @@ -0,0 +1,182 @@ +import { + recreateTestTables, + testCluster, +} from "../../../pg/__tests__/test-utils"; +import { PgSchema } from "../../../pg/PgSchema"; +import { createVC } from "../../__tests__/test-utils"; +import { BaseEnt } from "../../BaseEnt"; +import { EntNotReadableError } from "../../errors/EntNotReadableError"; +import { AllowIf } from "../../rules/AllowIf"; +import { GLOBAL_SHARD } from "../../ShardAffinity"; +import { CanReadOutgoingEdge } from "../CanReadOutgoingEdge"; +import { OutgoingEdgePointsToVC } from "../OutgoingEdgePointsToVC"; +import { True } from "../True"; + +/** + * CanReadOutgoingEdge uses global ID cache to determine if an Ent is readable. + * If the same ID exists for a different Ent with different privacy rules, then + * VC could elevate access before fixing the bug. + * + * Setup: + * * TestUser + * * TestObject (only accessible to owners) + * * TestObjectShallow (accessible to anyone + * * TestObjectSecret (only accessible to TestObject owners) + * + * By reading TestObjectShallow first, the attacker could then read + * TestObjectSecret. Now, they cannot, since IDsCache uses the Ent class + * objectId() to prevent keys collision. + */ +class EntTestUser extends BaseEnt( + testCluster, + new PgSchema( + 'ent.can-read-outgoing-edge"user', + { + id: { type: String, autoInsert: "id_gen()" }, + }, + [], + ), +) { + static readonly CREATE = [ + `CREATE TABLE %T( + id bigint NOT NULL PRIMARY KEY + )`, + ]; + + static override configure() { + return new this.Configuration({ + shardAffinity: GLOBAL_SHARD, + privacyInferPrincipal: async (_vc, row) => row.id, + privacyLoad: [new AllowIf(new OutgoingEdgePointsToVC("id"))], + privacyInsert: [], + }); + } +} + +class EntTestObject extends BaseEnt( + testCluster, + new PgSchema( + 'ent.can-read-outgoing-edge"object', + { + id: { type: String, autoInsert: "id_gen()" }, + owner_id: { type: String }, + }, + [], + ), +) { + static readonly CREATE = [ + `CREATE TABLE %T( + id bigint NOT NULL PRIMARY KEY, + owner_id bigint NOT NULL + )`, + ]; + + static override configure() { + return new this.Configuration({ + shardAffinity: GLOBAL_SHARD, + privacyInferPrincipal: null, + privacyLoad: [ + new AllowIf(new CanReadOutgoingEdge("owner_id", EntTestUser)), + ], + privacyInsert: [], + }); + } +} + +class EntTestObjectShallow extends BaseEnt( + testCluster, + new PgSchema( + 'ent.can-read-outgoing-edge"object', + { + id: { type: String, autoInsert: "id_gen()" }, + owner_id: { type: String }, + }, + [], + ), +) { + static readonly CREATE = []; + + static override configure() { + return new this.Configuration({ + shardAffinity: GLOBAL_SHARD, + privacyInferPrincipal: null, + privacyLoad: [new AllowIf(new True())], + privacyInsert: [], // Insert is not permitted + }); + } +} + +class EntTestObjectSecret extends BaseEnt( + testCluster, + new PgSchema( + 'ent.can-read-outgoing-edge"object_secret', + { + id: { type: String, autoInsert: "id_gen()" }, + object_id: { type: String }, + secret: { type: String }, + }, + [], + ), +) { + static readonly CREATE = [ + `CREATE TABLE %T( + id bigint NOT NULL PRIMARY KEY, + object_id bigint NOT NULL UNIQUE, + secret text NOT NULL + )`, + ]; + + static override configure() { + return new this.Configuration({ + shardAffinity: GLOBAL_SHARD, + privacyInferPrincipal: null, + privacyLoad: [ + new AllowIf(new CanReadOutgoingEdge("object_id", EntTestObject)), + ], + privacyInsert: [], + }); + } +} + +beforeEach(async () => { + await recreateTestTables([EntTestUser, EntTestObject, EntTestObjectSecret]); +}); + +test("Accessing the full ent fails", async () => { + const vc = createVC(); + const owner = await EntTestUser.insertReturning(vc.toOmniDangerous(), {}); + const objectID = await EntTestObject.insert(vc.toOmniDangerous(), { + owner_id: owner.id, + }); + await EntTestObjectSecret.insert(vc.toOmniDangerous(), { + object_id: objectID, + secret: "swordfish", + }); + + const attacker = await EntTestUser.insertReturning(vc.toOmniDangerous(), {}); + await expect( + EntTestObjectSecret.select(attacker.vc, { object_id: objectID }, 1), + ).rejects.toThrow(EntNotReadableError); +}); + +test("Accessing the shallow ent first and then accessing the full ent fails", async () => { + const vc = createVC(); + const owner = await EntTestUser.insertReturning(vc.toOmniDangerous(), {}); + const objectID = await EntTestObject.insert(vc.toOmniDangerous(), { + owner_id: owner.id, + }); + await EntTestObjectSecret.insert(vc.toOmniDangerous(), { + object_id: objectID, + secret: "swordfish", + }); + + const attacker = await EntTestUser.insertReturning(vc.toOmniDangerous(), {}); + await expect( + EntTestObjectShallow.loadX(attacker.vc, objectID), + ).resolves.toMatchObject({ + owner_id: owner.id, + }); + await expect( + EntTestObjectSecret.select(attacker.vc, { object_id: objectID }, 1), + ).rejects.toThrow(EntNotReadableError); +}); diff --git a/src/pg/PgClient.ts b/src/pg/PgClient.ts index fe057bb..37e491d 100644 --- a/src/pg/PgClient.ts +++ b/src/pg/PgClient.ts @@ -439,18 +439,18 @@ export class PgClient extends Client { allowExitOnIdle: true, }), ) - .on("connect", (poolClient) => { + .on("connect", (poolConn) => { // Called only once, after the connection is 1st created. - const client = poolClient as PgClientConn; + const conn = poolConn as PgClientConn; // Initialize additional properties merged into the default PoolClient. const maxConnLifetimeMs = maybeCall(this.options.maxConnLifetimeMs) * jitter(maybeCall(this.options.maxConnLifetimeJitter)); - client.pool = pool!; - client.id = connId++; - client.queriesSent = 0; - client.closeAt = + conn.pool = pool!; + conn.id = connId++; + conn.queriesSent = 0; + conn.closeAt = maxConnLifetimeMs > 0 ? Date.now() + Math.round(maxConnLifetimeMs) : null; @@ -459,7 +459,10 @@ export class PgClient extends Client { // and the outside world as "unhandled error". Appending an additional // error handler to EventEmitter doesn't affect the existing error // handlers anyhow, so should be safe. - client.on("error", () => {}); + conn.on("error", () => {}); + this.options.loggers?.clientConnectedLogger?.({ + node: this.options, + }); }) .on("error", (error) => // Having this hook prevents node from crashing. diff --git a/src/pg/__tests__/PgSchema.islands-reconfig.test.ts b/src/pg/__tests__/PgSchema.islands-reconfig.test.ts index e50db5b..14b508a 100644 --- a/src/pg/__tests__/PgSchema.islands-reconfig.test.ts +++ b/src/pg/__tests__/PgSchema.islands-reconfig.test.ts @@ -135,12 +135,17 @@ test("low level (non-sharded) client queries are not retried if the client is en testCluster.options.loggers, "clientEndLogger", ); + const clientConnectedLoggerSpy = jest.spyOn( + testCluster.options.loggers, + "clientConnectedLogger", + ); testCluster.options.islands = () => [ { no: 0, nodes: [TEST_CONFIG, { ...TEST_CONFIG, nameSuffix: "modified" }] }, ]; await testCluster.rediscover(); expect(clientEndLoggerSpy).toBeCalled(); + expect(clientConnectedLoggerSpy).toBeCalled(); await waitForExpect(() => expect(oldReplica.isEnded()).toBeTruthy()); await expect( oldReplica.query({ diff --git a/src/pg/__tests__/test-utils.ts b/src/pg/__tests__/test-utils.ts index bdc5caa..9c412c9 100644 --- a/src/pg/__tests__/test-utils.ts +++ b/src/pg/__tests__/test-utils.ts @@ -299,6 +299,7 @@ export const testCluster = new Cluster({ clientQueryLogger: jest.fn(), runOnShardErrorLogger: jest.fn(), clientEndLogger: jest.fn(), + clientConnectedLogger: jest.fn(), }, shardNamer: new PgShardNamer({ nameFormat: "sh%04d",