@dooer/dooer-test-env 1.18.0 → 1.19.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.
@@ -15,15 +15,33 @@ const PROFILES = ['full', 'frontend', 'hq', 'booking']
15
15
  // `full` (everything staging runs) — compose profiles are additive, so we must name it explicitly.
16
16
  // See ENVIRONMENT-PLAN.md §6.
17
17
 
18
+ // Regenerate when the generator is newer than the file it produced. It used to generate only when the
19
+ // file was ABSENT, which meant a CLI upgrade never reached an existing environment: every generator fix
20
+ // (a container flag, a new service, an image bump) silently applied to new installs only, and the way you
21
+ // found out was a bug that was already fixed. An npm install rewrites lib/, so its mtime moves ahead of a
22
+ // compose file generated by the previous version, and a local edit to the generator does the same.
23
+ // The compose file is a build artifact — anything hand-edited there was already lost on the next `db pull`.
24
+ function composeIsStale() {
25
+ if (!fs.existsSync(rt.COMPOSE_FILE)) return true
26
+ try {
27
+ const generator = require.resolve('../compose/generate')
28
+ return fs.statSync(generator).mtimeMs > fs.statSync(rt.COMPOSE_FILE).mtimeMs
29
+ } catch (_) {
30
+ return false // never block `up` on a stat that failed
31
+ }
32
+ }
33
+
18
34
  function ensureCompose() {
19
35
  rt.ensureRunDir()
20
- if (!fs.existsSync(rt.COMPOSE_FILE)) {
36
+ if (composeIsStale()) {
37
+ const existed = fs.existsSync(rt.COMPOSE_FILE)
21
38
  const r = generateCompose({
22
39
  servicesDir: rt.SERVICES_DIR,
23
40
  out: rt.COMPOSE_FILE,
24
41
  outputValidation: !!rt.readState().outputValidation,
25
42
  })
26
- console.log(chalk.gray(`generated ${rt.COMPOSE_FILE} (${(r && r.serviceCount) || 'n'} services)`))
43
+ const what = existed ? 'regenerated (CLI is newer)' : 'generated'
44
+ console.log(chalk.gray(`${what} ${rt.COMPOSE_FILE} (${(r && r.serviceCount) || 'n'} services)`))
27
45
  }
28
46
  return rt.COMPOSE_FILE
29
47
  }
@@ -242,6 +242,12 @@ function infraServices() {
242
242
  'ssl_cert_file=/etc/postgresql/server.crt',
243
243
  '-c',
244
244
  'ssl_key_file=/etc/postgresql/server.key',
245
+ // A full Dooer DB is ~163 schemas / 25k tables+sequences, and `db snapshot` runs pg_dump, which
246
+ // takes an ACCESS SHARE lock on every one of them inside a single transaction. The default 64
247
+ // gives ~64×(max_connections+prepared) lock slots and pg_dump dies partway with "out of shared
248
+ // memory / You might need to increase max_locks_per_transaction" — reliably, on a real base DB.
249
+ '-c',
250
+ 'max_locks_per_transaction=1024',
245
251
  ],
246
252
  environment: {
247
253
  POSTGRES_DB: 'dooer',
package/lib/copy.js CHANGED
@@ -93,6 +93,32 @@ async function copyOrganization({ organizationId, sourceEnv, targetEnv, reason,
93
93
  ` ${detail.rows} rows across ${detail.tables} tables`
94
94
  )
95
95
  console.log(` new organization id: ${chalk.bold(detail.organizationId)}`)
96
+
97
+ // Parent rows carried alongside the organization: global rows (workflow definitions, system messages)
98
+ // that its rows point at and the target may not have, because a target's base DB is a point-in-time
99
+ // snapshot while the source keeps moving. Only what was MISSING is inserted.
100
+ const refs = detail.references
101
+ if (refs && refs.rowsInArtifact) {
102
+ console.log(
103
+ ` referenced rows carried: ${refs.rowsInArtifact} from ${refs.tables.length} table(s)` +
104
+ `, ${refs.rowsInserted} inserted (the rest the target already had)`
105
+ )
106
+ }
107
+
108
+ // Rows whose foreign key points at a parent that did not come with them. The load runs with FK
109
+ // enforcement off (no ordering of 336 tables satisfies every constraint), and Postgres never re-checks,
110
+ // so these commit silently — they have to be told, or the copy looks clean when it is not.
111
+ if (detail.danglingRows) {
112
+ console.log(chalk.yellow(`\n ${detail.danglingRows} row(s) still reference a parent that is not there:`))
113
+ for (const d of detail.dangling || []) {
114
+ console.log(` ${String(d.rows).padStart(6)} ${d.from} → ${d.to} (${d.cause})`)
115
+ }
116
+ console.log(
117
+ ' The copy is complete and usable; these references dangle. Reference closure carries the parents\n' +
118
+ ' a copied row points at, so anything left here is a table it deliberately does not carry\n' +
119
+ ' (service_accounts.users / companies) or one it could not follow — see the warnings above.'
120
+ )
121
+ }
96
122
  console.log(` audit: export ${exported.id} (${source.name}), import ${imported.id} (${target.name})\n`)
97
123
 
98
124
  return { organizationId: detail.organizationId, rows: detail.rows, tables: detail.tables, skipped }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dooer/dooer-test-env",
3
- "version": "1.18.0",
3
+ "version": "1.19.0",
4
4
  "description": "Run the whole Dooer backend locally (staging DB minus customers), copy/purge customers between environments, and shred — one CLI.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": "Dooer/cli-dooer-test-env",
package/readme.md CHANGED
@@ -254,10 +254,44 @@ npx @dooer/dooer-test-env@latest customer copy \
254
254
  # optional: shred the rest of the PII afterwards (localhost only)
255
255
  npx @dooer/dooer-test-env@latest shred --execute
256
256
 
257
- # undo: delete that org again (rows + its S3 objects), dry-run first
257
+ # undo: delete that org's rows again dry-run first (omit --execute)
258
258
  npx @dooer/dooer-test-env@latest customer purge --org <orgId> --local --execute
259
259
  ```
260
260
 
261
+ **What the purge does not do:** it deletes rows, **not S3 objects**. A purged org leaves its uploaded
262
+ documents in the bucket; the command says so in its own output. Removing them needs bucket credentials the
263
+ service does not hold today.
264
+
265
+ **Referenced rows come with the org.** A copied row can point at a row in a table the schema map does not
266
+ copy: `service_workflow.run` is org-scoped and copied, but the workflow `definition` it points at is
267
+ global and is not. The target usually has those already — every environment's base DB descends from the
268
+ same production dump — but a base DB is a point-in-time snapshot while the source keeps moving. Production
269
+ has 819 workflow definitions; a base DB built in April 2023 has 689, so a 2026 org's runs pointed at
270
+ versions that were not there.
271
+
272
+ So the export carries the exact parent rows the org's rows reach, transitively (a carried `definition`
273
+ points at a `workflow` in turn), and the import inserts only what is missing:
274
+
275
+ ```
276
+ referenced rows carried: 9 from 2 table(s), 8 inserted (the rest the target already had)
277
+ ```
278
+
279
+ Their ids are **not** remapped — the copied rows reference them verbatim.
280
+
281
+ Two tables are never carried: **`service_accounts.users`** (it would move real names and personnummer from
282
+ the source environment into the target — exactly what a live→staging copy must not do) and
283
+ **`service_accounts.companies`** (the organization itself). The closure is capped at 50,000 rows, and both
284
+ the cap and any composite-key foreign key it declines to follow are reported as warnings.
285
+
286
+ Anything that still dangles is reported after the load, because the bulk load runs with foreign-key
287
+ enforcement off and Postgres never re-checks afterwards — so a dangling row would otherwise commit
288
+ silently:
289
+
290
+ ```
291
+ 44 row(s) still reference a parent that is not there:
292
+ 42 service_workflow.run.definitionId → service_workflow.definition
293
+ ```
294
+
261
295
  Naming: with **neither `--name` nor `--target`** the copy creates a new org that **keeps the source org's
262
296
  own name** (its `short_name` still gets a unique suffix). Pass `--name` to rename it, or `--target <uuid>`
263
297
  to copy into an org that already exists.