@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.
- package/lib/command/customer.js +57 -2
- package/lib/engine/seed.js +21 -6
- package/package.json +1 -1
- package/readme.md +42 -3
package/lib/command/customer.js
CHANGED
|
@@ -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',
|
|
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) =>
|
|
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',
|
package/lib/engine/seed.js
CHANGED
|
@@ -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
|
-
|
|
461
|
-
|
|
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(
|
|
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)
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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:
|
|
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)
|
|
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
|
-
|
|
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
|
|