@dooer/dooer-test-env 1.18.1 → 1.19.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/lib/command/env.js +29 -2
- package/lib/copy.js +18 -3
- package/package.json +1 -1
- package/readme.md +25 -9
package/lib/command/env.js
CHANGED
|
@@ -5,6 +5,7 @@ const registry = require('../registry')
|
|
|
5
5
|
const bankid = require('../bankid')
|
|
6
6
|
const { generateCompose } = require('../compose/generate')
|
|
7
7
|
const discovery = require('../discovery/client')
|
|
8
|
+
const envSpec = require('../env-spec')
|
|
8
9
|
|
|
9
10
|
// Every profile the generator emits (lib/compose/generate.js). `down` must name them all, or compose
|
|
10
11
|
// silently leaves the profiled containers running.
|
|
@@ -21,11 +22,17 @@ const PROFILES = ['full', 'frontend', 'hq', 'booking']
|
|
|
21
22
|
// found out was a bug that was already fixed. An npm install rewrites lib/, so its mtime moves ahead of a
|
|
22
23
|
// compose file generated by the previous version, and a local edit to the generator does the same.
|
|
23
24
|
// The compose file is a build artifact — anything hand-edited there was already lost on the next `db pull`.
|
|
25
|
+
//
|
|
26
|
+
// The environment spec counts as an input too. It carries the image tag of every service, so refreshing it
|
|
27
|
+
// and NOT regenerating leaves the stack pinned to whatever versions it was born with — which is how the
|
|
28
|
+
// local service-dooer-test-env sat at 3.1.1 while staging ran 3.2.1, and a purge failed with a foreign-key
|
|
29
|
+
// error that had been fixed weeks earlier.
|
|
24
30
|
function composeIsStale() {
|
|
25
31
|
if (!fs.existsSync(rt.COMPOSE_FILE)) return true
|
|
26
32
|
try {
|
|
27
|
-
const
|
|
28
|
-
|
|
33
|
+
const composedAt = fs.statSync(rt.COMPOSE_FILE).mtimeMs
|
|
34
|
+
const inputs = [require.resolve('../compose/generate'), require('../env-spec').SPEC_FILE]
|
|
35
|
+
return inputs.some((f) => fs.existsSync(f) && fs.statSync(f).mtimeMs > composedAt)
|
|
29
36
|
} catch (_) {
|
|
30
37
|
return false // never block `up` on a stat that failed
|
|
31
38
|
}
|
|
@@ -153,6 +160,26 @@ module.exports = [
|
|
|
153
160
|
for (const n of locals)
|
|
154
161
|
console.log(` ${n} → host.docker.internal:${state.local[n].port} (pid ${state.local[n].pid})`)
|
|
155
162
|
}
|
|
163
|
+
// Version pins from `service deploy`. They live in docker-compose.override.yml and win over the
|
|
164
|
+
// generated compose file forever — so a pinned service silently stays behind while everything around
|
|
165
|
+
// it moves, and `up` looks like it worked. Flagged when the pin no longer matches the environment
|
|
166
|
+
// spec, because that is the case where it costs you: the local service-dooer-test-env sat on 3.1.1
|
|
167
|
+
// through a spec refresh, a regenerate and a --force-recreate, failing with a bug fixed weeks before.
|
|
168
|
+
const pins = state.deploy || {}
|
|
169
|
+
const names = Object.keys(pins)
|
|
170
|
+
if (names.length) {
|
|
171
|
+
console.log(chalk.yellow('\nPinned to a specific version (`service deploy`):'))
|
|
172
|
+
for (const n of names) {
|
|
173
|
+
const current = envSpec.imageFor(n)
|
|
174
|
+
const behind = current && current !== pins[n]
|
|
175
|
+
console.log(
|
|
176
|
+
` ${n} → ${pins[n]}` +
|
|
177
|
+
(behind ? chalk.yellow(` ← the environment spec has ${current.split(':').pop()}`) : '')
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
console.log(chalk.gray(' clear a pin with: service deploy <name> <tag-from-the-spec>'))
|
|
181
|
+
}
|
|
182
|
+
|
|
156
183
|
try {
|
|
157
184
|
const reg = await discovery.list(rt.ROUTER_URL)
|
|
158
185
|
const overrides = Object.keys((reg && reg.overrides) || {})
|
package/lib/copy.js
CHANGED
|
@@ -94,17 +94,32 @@ async function copyOrganization({ organizationId, sourceEnv, targetEnv, reason,
|
|
|
94
94
|
)
|
|
95
95
|
console.log(` new organization id: ${chalk.bold(detail.organizationId)}`)
|
|
96
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
|
+
// Places the closure could not reach. Printed even though the copy succeeded: each one is a reference
|
|
108
|
+
// that may dangle, and a limitation nobody is told about is the same as one nobody fixed.
|
|
109
|
+
for (const w of (refs && refs.warnings) || []) console.log(chalk.yellow(` WARNING ${w}`))
|
|
110
|
+
|
|
97
111
|
// Rows whose foreign key points at a parent that did not come with them. The load runs with FK
|
|
98
112
|
// enforcement off (no ordering of 336 tables satisfies every constraint), and Postgres never re-checks,
|
|
99
113
|
// so these commit silently — they have to be told, or the copy looks clean when it is not.
|
|
100
114
|
if (detail.danglingRows) {
|
|
101
|
-
console.log(chalk.yellow(`\n ${detail.danglingRows} row(s) reference a parent that
|
|
115
|
+
console.log(chalk.yellow(`\n ${detail.danglingRows} row(s) still reference a parent that is not there:`))
|
|
102
116
|
for (const d of detail.dangling || []) {
|
|
103
117
|
console.log(` ${String(d.rows).padStart(6)} ${d.from} → ${d.to} (${d.cause})`)
|
|
104
118
|
}
|
|
105
119
|
console.log(
|
|
106
|
-
' The copy is complete and usable; these references dangle.
|
|
107
|
-
'
|
|
120
|
+
' The copy is complete and usable; these references dangle. Reference closure carries the parents\n' +
|
|
121
|
+
' a copied row points at, so anything left here is a table it deliberately does not carry\n' +
|
|
122
|
+
' (service_accounts.users / companies) or one it could not follow — see the warnings above.'
|
|
108
123
|
)
|
|
109
124
|
}
|
|
110
125
|
console.log(` audit: export ${exported.id} (${source.name}), import ${imported.id} (${target.name})\n`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dooer/dooer-test-env",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.1",
|
|
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
|
@@ -262,19 +262,35 @@ npx @dooer/dooer-test-env@latest customer purge --org <orgId> --local --execute
|
|
|
262
262
|
documents in the bucket; the command says so in its own output. Removing them needs bucket credentials the
|
|
263
263
|
service does not hold today.
|
|
264
264
|
|
|
265
|
-
**
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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:
|
|
269
274
|
|
|
270
275
|
```
|
|
271
|
-
|
|
272
|
-
42 service_workflow.run.definitionId → service_workflow.definition (parent table not in schema map)
|
|
273
|
-
2 service_accounts.systemMessageRecipient.systemMessageId → service_accounts.systemMessage
|
|
276
|
+
referenced rows carried: 9 from 2 table(s), 8 inserted (the rest the target already had)
|
|
274
277
|
```
|
|
275
278
|
|
|
276
|
-
|
|
277
|
-
|
|
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
|
+
```
|
|
278
294
|
|
|
279
295
|
Naming: with **neither `--name` nor `--target`** the copy creates a new org that **keeps the source org's
|
|
280
296
|
own name** (its `short_name` still gets a unique suffix). Pass `--name` to rename it, or `--target <uuid>`
|