@dooer/dooer-test-env 1.10.0 → 1.11.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.
@@ -1,3 +1,4 @@
1
+ const fs = require('fs')
1
2
  const chalk = require('chalk')
2
3
  const engine = require('../engine/seed')
3
4
  const { purge } = require('../engine/purge')
@@ -34,9 +35,37 @@ function toEngineArgv(argv) {
34
35
  return a
35
36
  }
36
37
 
38
+ // Resolve which orgs to copy: exactly one of --source / --sourcefile. A source FILE is inherently a batch,
39
+ // so --name and --target (which describe ONE target org) are rejected with it. (Jimmy 2026-09-03.)
40
+ function readSources(argv) {
41
+ if (argv.source && argv.sourcefile) throw new Error('pass either --source or --sourcefile, not both')
42
+ if (!argv.source && !argv.sourcefile) throw new Error('missing --source <uuid> or --sourcefile <path>')
43
+ if (!argv.sourcefile) return [argv.source]
44
+ if (argv.name || argv.target) {
45
+ throw new Error(
46
+ '--name and --target describe a single target org, so they cannot be combined with --sourcefile; ' +
47
+ 'each copied org keeps its own source name'
48
+ )
49
+ }
50
+ const raw = fs.readFileSync(argv.sourcefile, 'utf8')
51
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
52
+ const lines = raw
53
+ .split('\n')
54
+ .map((l) => l.trim())
55
+ .filter((l) => l && !l.startsWith('#'))
56
+ const bad = lines.filter((l) => !uuid.test(l))
57
+ if (bad.length) throw new Error(`${argv.sourcefile}: not org uuids: ${bad.slice(0, 3).join(', ')}`)
58
+ if (!lines.length) throw new Error(`${argv.sourcefile} contains no org uuids`)
59
+ return [...new Set(lines)]
60
+ }
61
+
37
62
  const copyOptions = (y) =>
38
63
  y
39
- .option('source', { type: 'string', demandOption: true, describe: 'org (companies_pk) to copy FROM' })
64
+ .option('source', { type: 'string', describe: 'org (companies_pk) to copy FROM' })
65
+ .option('sourcefile', {
66
+ type: 'string',
67
+ describe: 'file with one source org uuid per line — copies each in turn (no --name / --target)',
68
+ })
40
69
  .option('target', { type: 'string', describe: 'existing org to copy INTO' })
41
70
  .option('name', { type: 'string', describe: 'create a NEW target org with this name (needs --owner-user)' })
42
71
  .option('owner-user', { type: 'string', describe: 'existing user (users_pk) to own a newly-created org' })
@@ -66,7 +95,33 @@ module.exports = {
66
95
  command: 'copy',
67
96
  describe: 'copy one org into another (dry-run by default; emails anonymized)',
68
97
  builder: copyOptions,
69
- handler: (argv) => engine.main(engine.parseArgs(toEngineArgv(argv))),
98
+ handler: async (argv) => {
99
+ const sources = readSources(argv)
100
+ if (sources.length === 1) {
101
+ await engine.main(engine.parseArgs(toEngineArgv({ ...argv, source: sources[0] })))
102
+ return
103
+ }
104
+ // Batch: each org is a full independent copy (the engine scans the source per org). One bad uuid
105
+ // must not abandon the rest, so failures are collected and reported at the end.
106
+ console.log(chalk.bold(`\ncopying ${sources.length} organizations from ${argv.sourcefile}\n`))
107
+ const failed = []
108
+ for (let i = 0; i < sources.length; i++) {
109
+ console.log(chalk.cyan(`\n──── [${i + 1}/${sources.length}] ${sources[i]} ────`))
110
+ try {
111
+ await engine.main(engine.parseArgs(toEngineArgv({ ...argv, source: sources[i] })))
112
+ } catch (e) {
113
+ failed.push({ source: sources[i], error: e.message })
114
+ console.log(chalk.red(` FAILED: ${e.message}`))
115
+ }
116
+ }
117
+ console.log(
118
+ chalk.bold(
119
+ `\n${sources.length - failed.length}/${sources.length} organizations copied` + (failed.length ? ':' : '')
120
+ )
121
+ )
122
+ failed.forEach((f) => console.log(chalk.red(` ${f.source}: ${f.error}`)))
123
+ if (failed.length) process.exitCode = 1
124
+ },
70
125
  })
71
126
  .command({
72
127
  command: 'purge',
@@ -457,8 +457,12 @@ async function main(passedArgs) {
457
457
  const args = passedArgs || parseArgs(process.argv.slice(2))
458
458
  if (args.help) return void console.log(HELP)
459
459
  if (!args.source) throw new Error('missing --source')
460
- if (!args.target && !args.name) throw new Error('provide --target <uuid>, or --name to create a new target org')
461
- if (!args.target && !args.ownerUser) throw new Error('creating a new target org requires --owner-user')
460
+ // --name is OPTIONAL: with neither --target nor --name we create a new org and inherit the SOURCE org's
461
+ // name (resolved in createTargetOrg, which already has the source row). (Jimmy 2026-09-03.)
462
+ if (!args.target && !args.ownerUser)
463
+ throw new Error(
464
+ 'creating a new target org requires --owner-user (or pass --target <uuid> to copy into an existing org)'
465
+ )
462
466
  if (args.targetNamespace === 'dooer-production' && args.execute && !args.confirmProduction)
463
467
  throw new Error('refusing to WRITE to dooer-production without --confirm-production')
464
468
 
@@ -468,7 +472,11 @@ async function main(passedArgs) {
468
472
 
469
473
  console.log(`\n=== seed-test-account · ${mode} ===`)
470
474
  console.log(`source: ${args.source} @ ${args.sourceNamespace}`)
471
- console.log(`target: ${args.target || `(create "${args.name}", owner ${args.ownerUser})`} @ ${args.targetNamespace}`)
475
+ console.log(
476
+ `target: ${
477
+ args.target || `(create ${args.name ? `"${args.name}"` : "with the source's own name"}, owner ${args.ownerUser})`
478
+ } @ ${args.targetNamespace}`
479
+ )
472
480
  console.log(
473
481
  `email scrub: ${args.emailScrub} · copy tables: ${copyTables.length} · if-target-nonempty: ${args.ifTargetNonempty}\n`
474
482
  )
@@ -798,7 +806,12 @@ async function main(passedArgs) {
798
806
  await tgt.client.query("SET session_replication_role = 'origin'")
799
807
  await tgt.client.query('COMMIT')
800
808
  console.log(`\nEXECUTE complete — ${written} rows written to ${targetOrg} @ ${args.targetNamespace}.`)
801
- if (createdOrg) console.log(`Created org ${targetOrg} ("${args.name}") with owner ${args.ownerUser} (role Owner).`)
809
+ if (createdOrg)
810
+ console.log(
811
+ `Created org ${targetOrg}${args.name ? ` ("${args.name}")` : ' (name inherited from the source)'} with owner ${
812
+ args.ownerUser
813
+ } (role Owner).`
814
+ )
802
815
  // copy the actual files (S3) for every copied blob row — on by default (skip with --skip-files)
803
816
  if (!args.skipFiles) await copyFiles(src, args, map, remapUuid)
804
817
  else
@@ -860,15 +873,17 @@ async function createTargetOrg(src, tgt, args, targetOrg) {
860
873
  )
861
874
  ).rows[0]
862
875
  if (!row) throw new Error(`source companies row ${args.source} not found`)
876
+ // No --name given → keep the source org's own name (short_name still gets a unique suffix below).
877
+ const name = args.name || row.company_name || 'Copied organization'
863
878
  const slug =
864
- args.name
879
+ name
865
880
  .toLowerCase()
866
881
  .replace(/[^a-z0-9]+/g, '-')
867
882
  .replace(/^-|-$/g, '')
868
883
  .slice(0, 40) || 'org'
869
884
  const overrides = {
870
885
  companies_pk: targetOrg,
871
- company_name: args.name,
886
+ company_name: name,
872
887
  short_name: `${slug}-${newUuid().slice(0, 8)}`, // short_name is UNIQUE
873
888
  api_uuid: newUuid(),
874
889
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dooer/dooer-test-env",
3
- "version": "1.10.0",
3
+ "version": "1.11.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
@@ -63,13 +63,26 @@ particular machine — you `cd` to your own checkouts, and state lives in `~/.do
63
63
  | [localhost:4000](http://localhost:4000) | **public-facade** — the browser-facing entrypoint (GraphQL + REST passthrough) |
64
64
  | [localhost:4001](http://localhost:4001) | service-graphql (direct; frontends go via the facade) |
65
65
  | [localhost:8080](http://localhost:8080) | frontend-hq |
66
- | [localhost:8082/tools/graphiql](http://localhost:8082/tools/graphiql) | frontend-back-office-neue — handy GraphiQL console |
66
+ | [localhost:8081](http://localhost:8081) | frontend-ai-data-studio |
67
+ | [localhost:8082](http://localhost:8082) | frontend-back-office-neue |
68
+ | [localhost:8082/tools/graphiql](http://localhost:8082/tools/graphiql) | ↳ its GraphiQL console — handy for running queries by hand |
69
+ | [localhost:8083](http://localhost:8083) | frontend-billing-invoice |
70
+ | [localhost:8084](http://localhost:8084) | frontend-booking-engine |
71
+ | [localhost:8085](http://localhost:8085) | frontend-byra |
72
+ | [localhost:8087](http://localhost:8087) | frontend-microservice-documentation |
73
+ | [localhost:8088](http://localhost:8088) | frontend-reconcile-engine |
67
74
  | [localhost:8089](http://localhost:8089) | frontend-sumify-neue |
68
- | [localhost:8090](http://localhost:8090) · [8092](http://localhost:8092) · [8081](http://localhost:8081) | frontend-tasks · frontend-xrays · frontend-ai-data-studio |
75
+ | [localhost:8090](http://localhost:8090) | frontend-tasks |
76
+ | [localhost:8091](http://localhost:8091) | frontend-website |
77
+ | [localhost:8092](http://localhost:8092) | frontend-xrays |
69
78
  | [localhost:9001](http://localhost:9001) | MinIO console (`minioadmin` / `minioadmin`) |
70
79
  | `localhost:55432` | Postgres (`dooer` / `dooer`) |
80
+ | `localhost:19092` | Kafka/Redpanda, host listener (containers use `kafka:9092`) |
71
81
  | `localhost:21000`+ | every backend service, one host port each (so host processes can reach them) |
72
82
 
83
+ Frontend ports are assigned in manifest order, so adding a `frontend-*.yaml` can shift them —
84
+ `status` always shows the live mapping.
85
+
73
86
  ### Spin up an empty, ready-to-use customer
74
87
 
75
88
  ```bash
@@ -114,7 +127,33 @@ npx @dooer/dooer-test-env@latest shred --execute
114
127
  npx @dooer/dooer-test-env@latest customer purge --org <orgId> --local --execute
115
128
  ```
116
129
 
117
- It scans all ~336 tables in the source before writing, so expect a few minutes on a production source.
130
+ Naming: with **neither `--name` nor `--target`** the copy creates a new org that **keeps the source org's
131
+ own name** (its `short_name` still gets a unique suffix). Pass `--name` to rename it, or `--target <uuid>`
132
+ to copy into an org that already exists.
133
+
134
+ **Many orgs at once** — `--sourcefile` takes a file with one org uuid per line (`#` comments and blank
135
+ lines ignored, duplicates collapsed). Each org is copied in turn and keeps its own name, so `--name` and
136
+ `--target` (which describe a single target) are rejected with it:
137
+
138
+ ```bash
139
+ cat > orgs.txt <<'IDS'
140
+ e45a3bc4-1b61-4f55-9bd6-de2705420cc2
141
+ 0391c6ba-2c41-458b-95d2-a2d555117833
142
+ IDS
143
+
144
+ npx @dooer/dooer-test-env@latest customer copy \
145
+ --sourcefile orgs.txt \
146
+ --source-namespace dooer-production \
147
+ --target-local \
148
+ --owner-user <local-customer-users_pk> \
149
+ --execute
150
+ ```
151
+
152
+ One failure does not abandon the batch: failures are collected and summarised at the end (and the command
153
+ exits non-zero).
154
+
155
+ It scans all ~336 tables in the source before writing, so expect a few minutes per org on a production
156
+ source.
118
157
 
119
158
  ### Run your own code — one service, or several at once
120
159