@dudousxd/nestjs-catalog 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -75,6 +75,58 @@ export interface CatalogEnvironment {
75
75
  * `obj_<type>`) and carry no environment in them, so two environments in one
76
76
  * database would collide on every table. Separate databases make the
77
77
  * collision impossible and make MySQL's own `GRANT` the enforcement point.
78
+ *
79
+ * ## PostgreSQL keeps this exactly, and there the choice is a real one
80
+ *
81
+ * The sentence above leans on MySQL not distinguishing the two words. Postgres
82
+ * does distinguish them, and in a way that looks like an invitation: one
83
+ * connection reaches many schemas, which MySQL cannot do, so each environment
84
+ * could be a schema in one database behind one connection pool. That is
85
+ * cheaper — one pool instead of N — and it is refused.
86
+ *
87
+ * **The property that has to survive is the one this file is built around:
88
+ * there is no ambient default, and a cross-environment read is impossible
89
+ * because the database makes it so rather than because the application was
90
+ * careful.** Schema-per-environment cannot keep it, by either of the two
91
+ * routes available.
92
+ *
93
+ * *Route one, `search_path`.* Isolation then rests on a session variable on a
94
+ * pooled connection — and that is not a hypothetical hazard here, it is one
95
+ * this codebase has already measured and written up. `runReadOnlyQuery` in the
96
+ * MikroORM store deliberately passes its statement timeout as a per-statement
97
+ * hint rather than `SET SESSION`, because the session form was observed riding
98
+ * the pooled connection into an unrelated request: "after one query-console
99
+ * request, a *different* `em.fork()` read `@@SESSION.MAX_EXECUTION_TIME` back
100
+ * as the value set here". A leaked `MAX_EXECUTION_TIME` is a slow query. A
101
+ * leaked `search_path` is dev's request answered out of production's schema,
102
+ * returning entirely plausible rows, which is the failure
103
+ * {@link resolveEnvironment} exists to make impossible.
104
+ *
105
+ * *Route two, qualify every identifier.* Then isolation is a prefix that has to
106
+ * be present on every statement — which is the same class of thing as a
107
+ * `WHERE` clause, and this interface's own docblock says why that is refused:
108
+ * "there is no field here that a `WHERE` clause could be built out of, because
109
+ * a filter is precisely the sort of isolation that fails silently the one time
110
+ * somebody forgets it". A forgotten schema qualifier is a forgotten filter.
111
+ *
112
+ * And `GRANT` cannot rescue either route, which is the argument that actually
113
+ * settles it. One connection pool is one role; if that role can reach both
114
+ * schemas — which is what "one connection reaches many schemas" *means* — then
115
+ * `GRANT` is enforcing nothing between them. Making it the enforcement point
116
+ * again requires a role per environment, a role per environment requires a
117
+ * connection per environment, and at that point the shared pool that motivated
118
+ * the whole idea is gone and separate databases cost nothing extra.
119
+ *
120
+ * **What it costs:** N connection pools on Postgres, the same as on MySQL, and
121
+ * no cross-environment SQL join. The second is a feature — data never moves
122
+ * between environments — and the first is the price of the guarantee.
123
+ *
124
+ * **What an operator has to know:** nothing new. The deployment story is the
125
+ * same on both engines, which is the main thing this choice buys: one
126
+ * `catalogDatabaseNameFor`, one `ensureDatabase`, one shape of `GRANT`, and no
127
+ * per-engine paragraph in a runbook. The genuine Postgres/MySQL differences
128
+ * live in the store's `dialect.ts` and are about column case and search, not
129
+ * about isolation.
78
130
  */
79
131
  databaseName: string;
80
132
  /**
@@ -77,15 +77,43 @@ const ENVIRONMENT_ID_PATTERN = /^[a-z][a-z0-9_]{0,23}$/;
77
77
  * module refuses it as a tenant: "default" is the value that means "no
78
78
  * namespace at all", so an environment called `default` would derive the bare
79
79
  * keyspace and quietly share a results queue with every other engine on the
80
- * Redis. The rest are MySQL's own schemas, which an environment must never be
81
- * pointed at.
80
+ * Redis.
81
+ *
82
+ * The rest are databases the *engine* owns, and an environment must never be
83
+ * pointed at one — an environment id becomes a database name, so `mysql` or
84
+ * `postgres` here means this package running `CREATE TABLE catalog_object_type`
85
+ * inside the server's own maintenance database.
86
+ *
87
+ * **Both engines' lists, on both engines, deliberately.** The alternative is a
88
+ * refusal that depends on which driver happens to be mounted, and that is the
89
+ * shape that bites during a migration: an environment named `postgres` is
90
+ * perfectly legal on MySQL today and becomes a live incident on the day somebody
91
+ * moves the deployment, at which point renaming an environment means renaming
92
+ * its database, its MikroORM context and its Redis keyspace. Refusing the union
93
+ * costs a deployment nothing — nobody wants an environment called
94
+ * `information_schema` — and keeps the answer the same everywhere.
95
+ *
96
+ * `template0` and `template1` are Postgres's own, and are the two most likely to
97
+ * be typed by accident by somebody who has just read a `createdb` man page.
82
98
  */
83
99
  const RESERVED_ENVIRONMENT_IDS = [
84
100
  'default',
101
+ // MySQL's.
85
102
  'information_schema',
86
103
  'mysql',
87
104
  'performance_schema',
88
105
  'sys',
106
+ // PostgreSQL's. `postgres` is the maintenance database every cluster ships
107
+ // with and the one a client connects to when it has nowhere else to go.
108
+ 'postgres',
109
+ 'template0',
110
+ 'template1',
111
+ // Not a database but a schema, and reserved because a Postgres deployment
112
+ // that ever did put environments in schemas would collide with the default
113
+ // one — see the note on `databaseName` for why this package does not.
114
+ 'public',
115
+ 'pg_catalog',
116
+ 'pg_toast',
89
117
  ];
90
118
  function isEnvironmentId(value) {
91
119
  return (typeof value === 'string' &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dudousxd/nestjs-catalog",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "A metadata registry for NestJS: object types, properties and relations, derived from your ORM and enriched with decorators.",
5
5
  "license": "MIT",
6
6
  "author": "Davide Carvalho",