diff --git a/.gitignore b/.gitignore
index 72ad00e6c7..a4a7a2879f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
# IntelliJ project files
-.idea
+.idea/**
+!.idea/php.xml
+!.idea/.name
# Logs
logs
diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000000..100e370cac
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+VOL Monorepository
\ No newline at end of file
diff --git a/.idea/php.xml b/.idea/php.xml
new file mode 100644
index 0000000000..bfa7ae1eff
--- /dev/null
+++ b/.idea/php.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.lintstagedrc.mjs b/.lintstagedrc.mjs
index 0e97d9a419..2494ce2e36 100644
--- a/.lintstagedrc.mjs
+++ b/.lintstagedrc.mjs
@@ -1,6 +1,6 @@
import chalk from "chalk";
-import path from "path";
-import { execSync } from "child_process";
+import path from "node:path";
+import { execSync } from "node:child_process";
const generateTerraformDocs = (filenames) => {
try {
diff --git a/app/api/config/autoload/config.global.php b/app/api/config/autoload/config.global.php
index 1105136891..6318d9bbfc 100644
--- a/app/api/config/autoload/config.global.php
+++ b/app/api/config/autoload/config.global.php
@@ -54,13 +54,8 @@
],
'export' => [
'driverClass' => \Doctrine\DBAL\Driver\PDO\MySQL\Driver::class,
- 'params' => $doctrine_connection_params +
- [
- 'driverOptions' => [
- PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false,
- PDO::CURSOR_FWDONLY => true,
- ],
- ],
+ // Database connection details
+ 'params' => $doctrine_connection_params,
],
],
'driver' => [
diff --git a/app/api/config/autoload/local.php.dist b/app/api/config/autoload/local.php.dist
index bdbd217e20..5710271e5b 100644
--- a/app/api/config/autoload/local.php.dist
+++ b/app/api/config/autoload/local.php.dist
@@ -346,12 +346,14 @@ return [
'client' => [ // Guzzle client options; see https://docs.guzzlephp.org/en/stable/quickstart.html
'base_uri' => '', //param
'headers' => [], // additional or override default client headers
+ 'proxy' => new \Laminas\Stdlib\ArrayUtils\MergeRemoveKey(),
],
'oauth2' => [ // if client['headers']['Authorization'] is not set, then this will be used to get token
'client_id' => '', //param
'client_secret' => '', // secret
'token_url' => '', //param
'scope' => '', //param
+ 'proxy' => new \Laminas\Stdlib\ArrayUtils\MergeRemoveKey(),
],
],
];
diff --git a/app/internal/config/application.config.php b/app/internal/config/application.config.php
index be7488a7c8..71e27a0088 100644
--- a/app/internal/config/application.config.php
+++ b/app/internal/config/application.config.php
@@ -86,12 +86,4 @@
// 'service_manager' => array(),
];
-if (file_exists(__DIR__ . '/../vendor/laminas/laminas-developer-tools/src/Module.php')) {
- $config['modules'][] = 'Laminas\DeveloperTools';
-
- if (file_exists(__DIR__ . '/../vendor/san/san-session-toolbar/src/Module.php')) {
- $config['modules'][] = 'SanSessionToolbar';
- }
-}
-
return $config;
diff --git a/app/internal/config/autoload/local.php.dist b/app/internal/config/autoload/local.php.dist
index 537e6bca75..8afe63d72c 100644
--- a/app/internal/config/autoload/local.php.dist
+++ b/app/internal/config/autoload/local.php.dist
@@ -18,7 +18,7 @@ return [
'backend' => [
'options' => [
// Backend service URI *Environment specific*
- 'route' => 'backend-nginx',
+ 'route' => 'api.local.olcs.dev-dvsacloud.uk',
]
]
]
@@ -31,11 +31,11 @@ return [
'endpoints' => [
// Backend service URI *Environment specific*
'backend' => [
- 'url' => 'backend-nginx',
+ 'url' => 'api.local.olcs.dev-dvsacloud.uk',
],
// Postcode/Address service URI *Environment specific*
'postcode' => [
- 'url' => 'backend-nginx',
+ 'url' => 'https://int.nonprod.address.dvsa.api.gov.uk/',
],
]
],
@@ -65,7 +65,7 @@ return [
],
// Asset path, URI to olcs-static (CSS, JS, etc] *Environment specific*
- 'asset_path' => 'http://localhost:7001',
+ 'asset_path' => 'http://cdn.local.olcs.dev-dvsacloud.uk/',
'openam' => [
'url' => 'http://olcs-auth.olcs.gov.uk:8081/secure/',
diff --git a/app/internal/config/development.config.php.dist b/app/internal/config/development.config.php.dist
index 1b69fc4bd3..b1bb4350a4 100644
--- a/app/internal/config/development.config.php.dist
+++ b/app/internal/config/development.config.php.dist
@@ -3,6 +3,8 @@
return [
// Additional modules to include when in development mode
'modules' => [
+ 'Laminas\DeveloperTools',
+ 'SanSessionToolbar',
],
// Configuration overrides during development mode
'module_listener_options' => [
diff --git a/app/selfserve/config/application.config.php b/app/selfserve/config/application.config.php
index d9f298179f..7298f6747e 100644
--- a/app/selfserve/config/application.config.php
+++ b/app/selfserve/config/application.config.php
@@ -69,12 +69,4 @@
],
];
-if (file_exists(__DIR__ . '/../vendor/laminas/laminas-developer-tools/src/Module.php')) {
- $config['modules'][] = 'Laminas\DeveloperTools';
-
- if (file_exists(__DIR__ . '/../vendor/san/san-session-toolbar/src/Module.php')) {
- $config['modules'][] = 'SanSessionToolbar';
- }
-}
-
return $config;
diff --git a/app/selfserve/config/autoload/local.php.dist b/app/selfserve/config/autoload/local.php.dist
index a492760ebe..3852900cf3 100644
--- a/app/selfserve/config/autoload/local.php.dist
+++ b/app/selfserve/config/autoload/local.php.dist
@@ -18,7 +18,7 @@ return [
'backend' => [
'options' => [
// Backend service URI *Environment specific*
- 'route' => 'backend-nginx',
+ 'route' => 'api.local.olcs.dev-dvsacloud.uk',
]
]
]
@@ -31,17 +31,17 @@ return [
'endpoints' => [
// Backend service URI *Environment specific*
'backend' => [
- 'url' => 'backend-nginx',
+ 'url' => 'api.local.olcs.dev-dvsacloud.uk',
],
// Postcode/Address service URI *Environment specific*
'postcode' => [
- 'url' => 'http://address.reg.olcs.dev-dvsacloud.uk/',
+ 'url' => 'https://int.nonprod.address.dvsa.api.gov.uk/',
],
]
],
// Asset path, URI to olcs-static (CSS, JS, etc) *Environment specific for local use http://127.0.0.1:7001*
- 'asset_path' => 'http://localhost:7001',
+ 'asset_path' => 'http://cdn.local.olcs.dev-dvsacloud.uk/',
'openam' => new \Laminas\Stdlib\ArrayUtils\MergeRemoveKey(),
diff --git a/app/selfserve/config/development.config.php.dist b/app/selfserve/config/development.config.php.dist
index 1b69fc4bd3..b1bb4350a4 100644
--- a/app/selfserve/config/development.config.php.dist
+++ b/app/selfserve/config/development.config.php.dist
@@ -3,6 +3,8 @@
return [
// Additional modules to include when in development mode
'modules' => [
+ 'Laminas\DeveloperTools',
+ 'SanSessionToolbar',
],
// Configuration overrides during development mode
'module_listener_options' => [
diff --git a/infra/docker/cli/Dockerfile b/infra/docker/cli/Dockerfile
index d0de99bd0b..72c8d3247c 100644
--- a/infra/docker/cli/Dockerfile
+++ b/infra/docker/cli/Dockerfile
@@ -47,6 +47,11 @@ RUN apk add --no-cache linux-headers $PHPIZE_DEPS \
&& docker-php-ext-enable xdebug \
&& apk del linux-headers $PHPIZE_DEPS
+RUN apk update && \
+ apk add --no-cache openldap-dev \
+ && docker-php-ext-install ldap \
+ && docker-php-ext-enable ldap
+
RUN \
# Disable OPCache in development.
echo "opcache.enable=0" >> ${PHP_INI_DIR}/conf.d/1000-php.ini \
diff --git a/package-lock.json b/package-lock.json
index 861d222d8f..ecbb95f79d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -431,6 +431,15 @@
"integrity": "sha512-xiNMgCuoy4mCL4JTywk9XFs5xpRUcKxtWEcMR6FNMtsgewYTIgIR+nvlP4A4iRCAzRsHMnPhvTRrzp4AGcRTEA==",
"dev": true
},
+ "node_modules/@types/cli-progress": {
+ "version": "3.11.6",
+ "resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.6.tgz",
+ "integrity": "sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/conventional-commits-parser": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz",
@@ -478,12 +487,12 @@
"dev": true
},
"node_modules/@types/node": {
- "version": "22.3.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.3.0.tgz",
- "integrity": "sha512-nrWpWVaDZuaVc5X84xJ0vNrLvomM205oQyLsRt7OHNZbSHslcWsvgFR7O7hire2ZonjLrWBbedmotmIlJDVd6g==",
+ "version": "22.4.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.1.tgz",
+ "integrity": "sha512-1tbpb9325+gPnKK0dMm+/LMriX0vKxf6RnB0SZUqfyVkQ4fMgUSySqhxE/y8Jvs4NyF1yHzTfG9KlnkIODxPKg==",
"dev": true,
"dependencies": {
- "undici-types": "~6.18.2"
+ "undici-types": "~6.19.2"
}
},
"node_modules/@types/prompts": {
@@ -671,6 +680,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/cli-progress": {
+ "version": "3.12.0",
+ "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz",
+ "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.2.3"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/cli-truncate": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz",
@@ -687,6 +708,29 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/cli-truncate/node_modules/emoji-regex": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
+ "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==",
+ "dev": true
+ },
+ "node_modules/cli-truncate/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -725,35 +769,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/cliui/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true
- },
- "node_modules/cliui/node_modules/is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/cliui/node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/cliui/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -1002,9 +1017,9 @@
}
},
"node_modules/emoji-regex": {
- "version": "10.3.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
- "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==",
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
"node_modules/env-paths": {
@@ -2196,20 +2211,47 @@
}
},
"node_modules/string-width": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
- "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
- "emoji-regex": "^10.3.0",
- "get-east-asian-width": "^1.0.0",
- "strip-ansi": "^7.1.0"
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
},
"engines": {
- "node": ">=18"
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
},
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "engines": {
+ "node": ">=8"
}
},
"node_modules/strip-ansi": {
@@ -2350,9 +2392,9 @@
}
},
"node_modules/undici-types": {
- "version": "6.18.2",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.18.2.tgz",
- "integrity": "sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ==",
+ "version": "6.19.6",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.6.tgz",
+ "integrity": "sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org==",
"dev": true
},
"node_modules/unicorn-magic": {
@@ -2405,6 +2447,29 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
+ "node_modules/wrap-ansi/node_modules/emoji-regex": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
+ "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==",
+ "dev": true
+ },
+ "node_modules/wrap-ansi/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -2459,56 +2524,6 @@
"node": ">=12"
}
},
- "node_modules/yargs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yargs/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true
- },
- "node_modules/yargs/node_modules/is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yargs/node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yargs/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
@@ -2531,14 +2546,17 @@
}
},
"packages/local-refresh": {
+ "name": "@vol-app/local-refresh",
"version": "1.0.0",
"license": "MIT",
"devDependencies": {
"@tsconfig/recommended": "^1.0.7",
+ "@types/cli-progress": "^3.11.6",
"@types/flat-cache": "^2.0.2",
"@types/prompts": "^2.4.9",
"@types/shelljs": "^0.8.15",
"chalk": "^4.1.2",
+ "cli-progress": "^3.12.0",
"commander": "^12.1.0",
"dedent": "^1.5.3",
"flat-cache": "^5.0.0",
diff --git a/package.json b/package.json
index c4439cd240..226c3b1703 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,6 @@
],
"scripts": {
"prepare": "husky",
- "refresh": "ts-node packages/local-refresh/index.ts"
+ "refresh": "npm run start --workspace @vol-app/local-refresh"
}
}
diff --git a/packages/local-refresh/actions/ComposerInstall.ts b/packages/local-refresh/actions/ComposerInstall.ts
deleted file mode 100644
index d56f31d2c1..0000000000
--- a/packages/local-refresh/actions/ComposerInstall.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import prompts from "prompts";
-import shell from "shelljs";
-import path from "path";
-import chalk from "chalk";
-import ActionInterface from "./ActionInterface";
-import createDebug from "debug";
-
-const debug = createDebug("refresh:actions:ComposerInstall");
-
-const phpAppDirectoryNames = ["api", "selfserve", "internal"];
-const phpAppDirectories = phpAppDirectoryNames.map((dir) => path.resolve(__dirname, `../../../app/${dir}`));
-
-export default class ComposerInstall implements ActionInterface {
- async prompt(): Promise {
- const isComposerInstalled = shell.exec("composer --version", { silent: !debug.enabled }).code === 0;
-
- if (!isComposerInstalled) {
- console.error(chalk.red("Error: Composer is not installed. Skipping Composer install..."));
- return false;
- }
-
- const response = await prompts({
- type: "confirm",
- name: "composer-install",
- message: "Install Composer dependencies?",
- });
-
- return response["composer-install"];
- }
-
- async execute(): Promise {
- phpAppDirectories.forEach((dir) => {
- debug(chalk.blue(`Running composer install in ${dir}...`));
-
- if (
- shell.exec("composer install --no-interaction --no-progress", {
- cwd: dir,
- silent: !debug.enabled,
- env: {
- ...process.env,
- FORCE_COLOR: "1",
- },
- }).code !== 0
- ) {
- console.error(chalk.red(`Error: Composer install failed in ${dir}`));
- }
- });
- }
-}
diff --git a/packages/local-refresh/actions/CopyAppDistFiles.ts b/packages/local-refresh/actions/CopyAppDistFiles.ts
deleted file mode 100644
index 64d58ef662..0000000000
--- a/packages/local-refresh/actions/CopyAppDistFiles.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import prompts from "prompts";
-import fs from "node:fs";
-import path from "path";
-import chalk from "chalk";
-import ActionInterface from "./ActionInterface";
-import createDebug from "debug";
-
-const debug = createDebug("refresh:actions:CopyAppDistFiles");
-
-const phpAppDirectoryNames = ["api", "selfserve", "internal"];
-const phpAppDirectories = phpAppDirectoryNames.map((dir) => path.resolve(__dirname, `../../../app/${dir}`));
-
-export default class ResetDatabase implements ActionInterface {
- filesToCopy: string[] = [];
-
- async prompt(): Promise {
- const response = await prompts({
- type: "confirm",
- name: "should-copy",
- message: "Copy the Laminas configuration dist files?",
- warn: "This will overwrite existing configuration files.",
- });
-
- if (!response["should-copy"]) {
- return false;
- }
-
- const appConfigDistFiles = phpAppDirectories
- .map((dir) => {
- const configDir = path.join(dir, "config");
-
- return fs
- .readdirSync(configDir, { recursive: true })
- .filter((file) => typeof file === "string")
- .map((fileName) => {
- return path.join(configDir, fileName);
- })
- .filter((fileName) => fs.lstatSync(fileName).isFile())
- .filter((fileName) => fileName.endsWith(".dist"));
- })
- .flat();
-
- this.filesToCopy = (
- await prompts({
- type: "multiselect",
- name: "files",
- message: "Which config files do you want to copy?",
- choices: appConfigDistFiles.map((file) => ({ title: file, value: file })),
- hint: "- Space to select. Return to submit",
- })
- ).files;
-
- return this.filesToCopy.length > 0;
- }
-
- async execute(): Promise {
- this.filesToCopy.forEach((file) => {
- const destination = file.replace(".dist", "");
-
- debug(chalk.greenBright(`Copying ${file} to ${destination}...`));
-
- fs.copyFileSync(file, destination);
- });
- }
-}
diff --git a/packages/local-refresh/actions/FlushRedis.ts b/packages/local-refresh/actions/FlushRedis.ts
deleted file mode 100644
index b94fe79428..0000000000
--- a/packages/local-refresh/actions/FlushRedis.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import prompts from "prompts";
-import chalk from "chalk";
-import ActionInterface from "./ActionInterface";
-import shell from "shelljs";
-
-export default class FlushRedis implements ActionInterface {
- async prompt(): Promise {
- const response = await prompts({
- type: "confirm",
- name: "should-flush",
- message: "Flush the Redis cache?",
- warn: "This will remove all cached data.",
- });
-
- return response["should-flush"];
- }
-
- async execute(): Promise {
- if (shell.exec(`docker compose exec redis redis-cli -c "FLUSHALL"`, { silent: true }).code !== 0) {
- console.error(chalk.red(`Error: Failed to flush Redis cache`));
- return;
- }
- }
-}
diff --git a/packages/local-refresh/actions/ResetDatabase.ts b/packages/local-refresh/actions/ResetDatabase.ts
deleted file mode 100644
index 98c4b7709b..0000000000
--- a/packages/local-refresh/actions/ResetDatabase.ts
+++ /dev/null
@@ -1,180 +0,0 @@
-import prompts from "prompts";
-import fs from "node:fs";
-import flatCache from "flat-cache";
-import shell from "shelljs";
-import chalk from "chalk";
-import ActionInterface from "./ActionInterface";
-import dedent from "dedent";
-import createDebug from "debug";
-
-const debug = createDebug("refresh:actions:ResetDatabase");
-
-const cache = flatCache.load("reset-database");
-
-enum DatabaseRefreshEnum {
- FULL,
- MIGRATIONS,
- NONE,
-}
-
-export default class ResetDatabase implements ActionInterface {
- bucketName = "devapp-olcs-pri-olcs-deploy-s3";
-
- etlDirectory: string | undefined;
- refreshType: DatabaseRefreshEnum = DatabaseRefreshEnum.NONE;
-
- async prompt(): Promise {
- const response = await prompts([
- {
- type: "confirm",
- name: "database-refresh",
- message: "Reset the database?",
- },
- {
- type: (prev) => (prev === true ? "select" : null),
- name: "refresh-type",
- message: "Choose the type of database reset?",
- choices: [
- { title: "Full refresh", value: DatabaseRefreshEnum.FULL },
- { title: "Just migrations", value: DatabaseRefreshEnum.MIGRATIONS },
- ],
- },
- ]);
-
- if (response["refresh-type"] === undefined) {
- return false;
- }
-
- this.refreshType = response["refresh-type"];
-
- const etlDirectoryPrompt = await prompts({
- type: "text",
- name: "directory",
- message: "Enter the path to the ETL directory",
- initial: cache.getKey("etlDirectory") || "../olcs-etl",
- validate: (value) => (fs.existsSync(value) ? true : "Path does not exist"),
- });
-
- if (etlDirectoryPrompt.directory === undefined) {
- return false;
- }
-
- cache.setKey("etlDirectory", etlDirectoryPrompt.directory);
- cache.save();
-
- this.etlDirectory = etlDirectoryPrompt.directory;
-
- return this.refreshType !== DatabaseRefreshEnum.NONE;
- }
-
- async execute(): Promise {
- // Full reset requires AWS credentials to pull down the anonymised dataset from S3.
- if (this.refreshType === DatabaseRefreshEnum.FULL) {
- if (shell.exec("aws sts get-caller-identity").code !== 0) {
- console.error(
- chalk.red(
- "Error: Valid AWS credentials are required for a full database reset. Authenticate with VOL `nonprod` and retry.",
- ),
- );
-
- return;
- }
- }
-
- const myCnf = dedent`
- [client]
- user=root
- password=olcs
- host=host.docker.internal
- port=3306`;
-
- if (this.refreshType !== DatabaseRefreshEnum.MIGRATIONS) {
- if (
- shell.exec(
- dedent`docker compose exec db /bin/bash -c "\
- echo '${myCnf}' > ~/.my.cnf; \
- cd /var/lib/etl \
- && ./create-base.sh olcs_be
- "
- `,
- {
- env: {
- ...process.env,
- FORCE_COLOR: "1",
- },
- },
- ).code !== 0
- ) {
- console.error(chalk.red(`Error: \`create-base.sh\` failed`));
- return;
- }
- }
-
- if (
- shell.exec(
- `docker run \
- --rm \
- -e INSTALL_MYSQL=true \
- -v "$PWD/${this.etlDirectory}/":/liquibase/changelog \
- -w /liquibase/changelog \
- liquibase/liquibase \
- --defaultsFile=liquibase.properties \
- update \
- -Ddataset=testdata`,
- {
- env: {
- ...process.env,
- FORCE_COLOR: "1",
- },
- },
- ).code !== 0
- ) {
- console.error(chalk.red(`Error: \`liquibase\` failed`));
- return;
- }
-
- // Fetch file from S3.
- const latestAnonDatasetCmd = shell.exec(
- `aws s3 ls s3://${this.bucketName}/anondata/olcs-db-localdev-anon-prod --recursive 2>/dev/null | sort | tail -n 1 | awk '{print $4}'`,
- {
- silent: !debug.enabled,
- },
- );
-
- if (latestAnonDatasetCmd.code !== 0) {
- console.error(chalk.red("Error: Could not find the latest anonymised dataset on S3"));
- return;
- }
-
- const latestAnonDataset = latestAnonDatasetCmd.stdout.trim();
-
- debug(chalk.greenBright(`Fetching the latest anonymised dataset from S3: ${latestAnonDataset}`));
-
- if (
- shell.exec(
- `aws s3 cp s3://${this.bucketName}/${latestAnonDataset} ${this.etlDirectory}/olcs-db-localdev-anon-prod.sql.gz`,
- ).code !== 0
- ) {
- console.error(chalk.red("Error: Could not fetch the latest anonymised dataset from S3"));
-
- shell.exec(`rm ${this.etlDirectory}/olcs-db-localdev-anon-prod.sql.gz`);
- return;
- }
-
- if (
- shell.exec(
- `docker compose exec -T db /bin/bash -c 'zcat /var/lib/etl/olcs-db-localdev-anon-prod.sql.gz | mysql -u mysql -polcs olcs_be'`,
- {
- silent: !debug.enabled,
- },
- ).code !== 0
- ) {
- console.error(chalk.red("Error: Could not import the anonymised dataset into the database"));
-
- shell.exec(`rm ${this.etlDirectory}/olcs-db-localdev-anon-prod.sql.gz`);
- return;
- }
-
- shell.exec(`rm ${this.etlDirectory}/olcs-db-localdev-anon-prod.sql.gz`);
- }
-}
diff --git a/packages/local-refresh/package.json b/packages/local-refresh/package.json
index d095390a37..78e2f70c57 100644
--- a/packages/local-refresh/package.json
+++ b/packages/local-refresh/package.json
@@ -5,10 +5,12 @@
"license": "MIT",
"devDependencies": {
"@tsconfig/recommended": "^1.0.7",
+ "@types/cli-progress": "^3.11.6",
"@types/flat-cache": "^2.0.2",
"@types/prompts": "^2.4.9",
"@types/shelljs": "^0.8.15",
"chalk": "^4.1.2",
+ "cli-progress": "^3.12.0",
"commander": "^12.1.0",
"dedent": "^1.5.3",
"flat-cache": "^5.0.0",
@@ -18,6 +20,7 @@
"typescript": "~5.5.2"
},
"scripts": {
- "prepare": "husky"
+ "prepare": "husky",
+ "start": "ts-node src/index.ts"
}
}
diff --git a/packages/local-refresh/actions/ActionInterface.ts b/packages/local-refresh/src/actions/ActionInterface.ts
similarity index 71%
rename from packages/local-refresh/actions/ActionInterface.ts
rename to packages/local-refresh/src/actions/ActionInterface.ts
index ba6e1c5ef3..11d7f8f267 100644
--- a/packages/local-refresh/actions/ActionInterface.ts
+++ b/packages/local-refresh/src/actions/ActionInterface.ts
@@ -1,3 +1,5 @@
+import { GenericBar } from "cli-progress";
+
export default interface ActionInterface {
/**
* Prompt the user for input.
@@ -9,5 +11,5 @@ export default interface ActionInterface {
/**
* Execute the action.
*/
- execute(): Promise;
+ execute(progressBar: GenericBar): Promise;
}
diff --git a/packages/local-refresh/src/actions/ComposerInstall.ts b/packages/local-refresh/src/actions/ComposerInstall.ts
new file mode 100644
index 0000000000..6245aa7c7b
--- /dev/null
+++ b/packages/local-refresh/src/actions/ComposerInstall.ts
@@ -0,0 +1,46 @@
+import prompts from "prompts";
+import exec from "../exec";
+import path from "node:path";
+import chalk from "chalk";
+import ActionInterface from "./ActionInterface";
+import createDebug from "debug";
+import { GenericBar } from "cli-progress";
+
+const debug = createDebug("refresh:actions:ComposerInstall");
+
+const phpAppDirectoryNames = ["api", "selfserve", "internal"];
+const phpAppDirectories = phpAppDirectoryNames.map((dir) => path.resolve(__dirname, `../../../../app/${dir}`));
+
+export default class ComposerInstall implements ActionInterface {
+ async prompt(): Promise {
+ const { shouldInstall } = await prompts({
+ type: "confirm",
+ name: "shouldInstall",
+ message: "Install Composer dependencies?",
+ });
+
+ return shouldInstall;
+ }
+
+ async execute(progress: GenericBar): Promise {
+ progress.start(phpAppDirectories.length, 0);
+
+ try {
+ exec("composer --version", debug);
+ } catch (e: unknown) {
+ throw new Error("Composer is not installed. Please install Composer before running this action.");
+ }
+
+ phpAppDirectories.forEach((dir) => {
+ debug(chalk.blue(`Running composer install in ${dir}...`));
+
+ exec("composer install --no-interaction --no-progress", debug, {
+ cwd: dir,
+ });
+
+ progress.increment();
+ });
+
+ progress.stop();
+ }
+}
diff --git a/packages/local-refresh/src/actions/CopyAppDistFiles.ts b/packages/local-refresh/src/actions/CopyAppDistFiles.ts
new file mode 100644
index 0000000000..2196298d05
--- /dev/null
+++ b/packages/local-refresh/src/actions/CopyAppDistFiles.ts
@@ -0,0 +1,89 @@
+import prompts from "prompts";
+import fs from "node:fs";
+import path from "node:path";
+import chalk from "chalk";
+import ActionInterface from "./ActionInterface";
+import createDebug from "debug";
+import { GenericBar } from "cli-progress";
+
+const debug = createDebug("refresh:actions:CopyAppDistFiles");
+
+const phpAppDirectoryNames = ["api", "selfserve", "internal"];
+const phpAppDirectories = phpAppDirectoryNames.map((dir) => path.resolve(__dirname, `../../../../app/${dir}`));
+
+export default class ResetDatabase implements ActionInterface {
+ filesToCopy: string[] = [];
+
+ async prompt(): Promise {
+ const { shouldCopy } = await prompts({
+ type: "confirm",
+ name: "shouldCopy",
+ message: "Copy the Laminas configuration dist files?",
+ warn: "This will overwrite existing configuration files.",
+ });
+
+ if (!shouldCopy) {
+ return false;
+ }
+
+ let appConfigDistFiles: Map = new Map();
+
+ for (const dir of phpAppDirectories) {
+ const configDir = path.join(dir, "config");
+
+ if (!fs.existsSync(configDir)) {
+ continue;
+ }
+
+ const files = fs
+ .readdirSync(configDir, { recursive: true })
+ .filter((file) => typeof file === "string")
+ .map((fileName) => {
+ return path.join(configDir, fileName);
+ })
+ .filter((fileName) => fs.lstatSync(fileName).isFile())
+ .filter((fileName) => fileName.endsWith(".dist"));
+
+ files.forEach((file) => {
+ const truncatedPath = file.replace(path.dirname(dir), "");
+
+ appConfigDistFiles.set(file, truncatedPath);
+ });
+ }
+
+ const { files } = await prompts({
+ type: "multiselect",
+ name: "files",
+ message: "Which config files do you want to copy?",
+ choices: Array.from(appConfigDistFiles.keys()).map((file) => ({
+ title: appConfigDistFiles.get(file) || file,
+ value: file,
+ })),
+ hint: "- Space to select. Return to submit",
+ });
+
+ if (!files) {
+ return false;
+ }
+
+ this.filesToCopy = files;
+
+ return this.filesToCopy.length > 0;
+ }
+
+ async execute(progress: GenericBar): Promise {
+ progress.start(this.filesToCopy.length, 0);
+
+ this.filesToCopy.forEach((file) => {
+ const destination = file.replace(".dist", "");
+
+ debug(chalk.greenBright(`Copying ${file} to ${destination}...`));
+
+ fs.copyFileSync(file, destination);
+
+ progress.increment();
+ });
+
+ progress.stop();
+ }
+}
diff --git a/packages/local-refresh/src/actions/FlushRedis.ts b/packages/local-refresh/src/actions/FlushRedis.ts
new file mode 100644
index 0000000000..74c6516c65
--- /dev/null
+++ b/packages/local-refresh/src/actions/FlushRedis.ts
@@ -0,0 +1,28 @@
+import prompts from "prompts";
+import ActionInterface from "./ActionInterface";
+import exec from "../exec";
+import createDebug from "debug";
+import { GenericBar } from "cli-progress";
+
+const debug = createDebug("refresh:actions:FlushRedis");
+
+export default class FlushRedis implements ActionInterface {
+ async prompt(): Promise {
+ const { shouldFlush } = await prompts({
+ type: "confirm",
+ name: "shouldFlush",
+ message: "Flush the Redis cache?",
+ warn: "This will remove all cached data.",
+ });
+
+ return shouldFlush;
+ }
+
+ async execute(progress: GenericBar): Promise {
+ progress.start(1, 0);
+
+ exec(`docker compose exec redis redis-cli -c "FLUSHALL"`, debug);
+
+ progress.stop();
+ }
+}
diff --git a/packages/local-refresh/src/actions/ResetDatabase.ts b/packages/local-refresh/src/actions/ResetDatabase.ts
new file mode 100644
index 0000000000..0c7dfca145
--- /dev/null
+++ b/packages/local-refresh/src/actions/ResetDatabase.ts
@@ -0,0 +1,226 @@
+import prompts from "prompts";
+import fs from "node:fs";
+import flatCache from "flat-cache";
+import path from "node:path";
+import exec from "../exec";
+import chalk from "chalk";
+import ActionInterface from "./ActionInterface";
+import dedent from "dedent";
+import createDebug from "debug";
+import { GenericBar } from "cli-progress";
+
+const debug = createDebug("refresh:actions:ResetDatabase");
+
+const cache = flatCache.load("reset-database");
+
+enum DatabaseRefreshEnum {
+ NONE,
+ FULL,
+ MIGRATIONS,
+}
+
+const liquibasePropertiesTemplate = dedent`
+ url: jdbc:mysql://host.docker.internal/olcs_be?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=utf8&connectionCollation=utf8_general_ci
+ username: root
+ password: olcs
+ changeLogFile: changesets/OLCS.xml
+ logLevel: error
+`;
+
+export default class ResetDatabase implements ActionInterface {
+ bucketName = "devapp-olcs-pri-olcs-deploy-s3";
+
+ etlDirectory = "../olcs-etl";
+ refreshType: DatabaseRefreshEnum = DatabaseRefreshEnum.NONE;
+
+ liquibasePropertiesFileName = `vol-app.liquibase.properties`;
+ createLiquibaseProperties = false;
+
+ async prompt(): Promise {
+ const { shouldResetDatabase, refreshType } = await prompts([
+ {
+ type: "confirm",
+ name: "shouldResetDatabase",
+ message: "Reset the database?",
+ },
+ {
+ type: (prev) => (prev === true ? "select" : null),
+ name: "refreshType",
+ message: "Choose the type of database reset?",
+ choices: [
+ { title: "Full refresh", value: DatabaseRefreshEnum.FULL },
+ { title: "Just migrations", value: DatabaseRefreshEnum.MIGRATIONS },
+ ],
+ },
+ ]);
+
+ if (shouldResetDatabase === undefined || refreshType === undefined) {
+ return false;
+ }
+
+ this.refreshType = refreshType;
+
+ const { directory } = await prompts({
+ type: "text",
+ name: "directory",
+ message: "Enter the path to the ETL directory",
+ initial: cache.getKey("etlDirectory") || this.etlDirectory,
+ validate: (value) =>
+ fs.existsSync(path.isAbsolute(value) ? value : path.resolve(__dirname, "../../../../", value))
+ ? true
+ : "Path does not exist",
+ });
+
+ if (typeof directory !== "string") {
+ return false;
+ }
+
+ this.etlDirectory = path.isAbsolute(directory) ? directory : path.resolve(__dirname, "../../../../", directory);
+
+ cache.setKey("etlDirectory", this.etlDirectory);
+ cache.save();
+
+ debug(
+ `Checking for liquibase properties file at: ${path.join(this.etlDirectory, this.liquibasePropertiesFileName)}`,
+ );
+
+ const liquibasePropertiesExists = fs.existsSync(path.join(this.etlDirectory, this.liquibasePropertiesFileName));
+
+ const liquibasePropertiesIsDifferent =
+ liquibasePropertiesExists &&
+ fs.readFileSync(path.join(this.etlDirectory, this.liquibasePropertiesFileName), "utf8") !==
+ liquibasePropertiesTemplate;
+
+ if (!liquibasePropertiesExists || liquibasePropertiesIsDifferent) {
+ const { createLiquibaseProperties } = await prompts({
+ type: "confirm",
+ name: "createLiquibaseProperties",
+ message:
+ liquibasePropertiesExists && liquibasePropertiesIsDifferent
+ ? "Liquibase properties file is out-of-date. Overwrite?"
+ : "Create liquibase properties file?",
+ });
+
+ if (!createLiquibaseProperties) {
+ return false;
+ }
+
+ this.createLiquibaseProperties = createLiquibaseProperties;
+ } else {
+ debug("Liquibase properties file already exists and is up-to-date. Skipping step.");
+ }
+
+ return this.refreshType !== DatabaseRefreshEnum.NONE;
+ }
+
+ async execute(progress: GenericBar): Promise {
+ const isFullRefresh = this.refreshType === DatabaseRefreshEnum.FULL;
+
+ progress.start(10, 0);
+
+ if (isFullRefresh) {
+ await this.#createBaseDatabase();
+ }
+
+ progress.increment(4);
+
+ if (this.createLiquibaseProperties) {
+ this.#createLiquidbasePropertiesFile();
+ }
+
+ progress.increment(1);
+
+ await this.#runLiquibaseUpdate();
+
+ progress.increment(5);
+
+ if (isFullRefresh) {
+ await this.#fetchAnonymisedDataset();
+ }
+
+ progress.stop();
+ }
+
+ async #createBaseDatabase(): Promise {
+ const myCnf = dedent`
+ [client]
+ user=root
+ password=olcs
+ host=host.docker.internal
+ port=3306`;
+
+ exec(
+ dedent`docker compose exec db /bin/bash -c "\
+ echo '${myCnf}' > ~/.my.cnf; \
+ cd /var/lib/etl \
+ && ./create-base.sh olcs_be
+ "`,
+ debug,
+ );
+ }
+
+ #createLiquidbasePropertiesFile(): void {
+ debug(`Creating liquibase properties file at: ${path.join(this.etlDirectory, this.liquibasePropertiesFileName)}`);
+
+ fs.writeFileSync(path.join(this.etlDirectory, this.liquibasePropertiesFileName), liquibasePropertiesTemplate);
+ }
+
+ async #runLiquibaseUpdate(): Promise {
+ exec(
+ `docker run \
+ --rm \
+ -e INSTALL_MYSQL=true \
+ -v "${this.etlDirectory}":/liquibase/changelog \
+ -w /liquibase/changelog \
+ liquibase/liquibase \
+ --defaultsFile=${this.liquibasePropertiesFileName} \
+ update \
+ -Ddataset=testdata
+ `,
+ debug,
+ );
+ }
+
+ async #fetchAnonymisedDataset(): Promise {
+ // Full reset requires AWS credentials to pull down the anonymised dataset from S3.
+ try {
+ exec("aws sts get-caller-identity", debug);
+ } catch (e: unknown) {
+ throw new Error(
+ "Valid AWS credentials are required for a full database reset. Authenticate with VOL `nonprod` and retry.",
+ );
+ }
+
+ const latestAnonDatasetCmd = exec(
+ `aws s3 ls s3://${this.bucketName}/anondata/olcs-db-localdev-anon-prod --recursive 2>/dev/null | sort | tail -n 1 | awk '{print $4}'`,
+ debug,
+ );
+
+ if (latestAnonDatasetCmd.code !== 0) {
+ throw new Error("Could not find the latest anonymised dataset on S3");
+ }
+
+ const latestAnonDataset = latestAnonDatasetCmd.stdout.trim();
+
+ const cleanUp = () => {
+ debug("Removing the anonymised dataset from the ETL directory");
+ exec(`rm ${path.join(this.etlDirectory, "/olcs-db-localdev-anon-prod.sql.gz")}`, debug);
+ };
+
+ debug(chalk.greenBright(`Fetching the latest anonymised dataset from S3: ${latestAnonDataset}`));
+
+ try {
+ exec(
+ `aws s3 cp s3://${this.bucketName}/${latestAnonDataset} ${path.join(this.etlDirectory, "/olcs-db-localdev-anon-prod.sql.gz")}`,
+ debug,
+ );
+
+ exec(
+ `docker compose exec -T db /bin/bash -c 'zcat /var/lib/etl/olcs-db-localdev-anon-prod.sql.gz | mysql -u mysql -polcs olcs_be'`,
+ debug,
+ );
+ } finally {
+ cleanUp();
+ }
+ }
+}
diff --git a/packages/local-refresh/actions/ResetLdap.ts b/packages/local-refresh/src/actions/ResetLdap.ts
similarity index 63%
rename from packages/local-refresh/actions/ResetLdap.ts
rename to packages/local-refresh/src/actions/ResetLdap.ts
index 42beb28fe5..7b19f1934a 100644
--- a/packages/local-refresh/actions/ResetLdap.ts
+++ b/packages/local-refresh/src/actions/ResetLdap.ts
@@ -1,34 +1,37 @@
import prompts from "prompts";
-import shell from "shelljs";
+import exec from "../exec";
import chalk from "chalk";
import dedent from "dedent";
import ActionInterface from "./ActionInterface";
import createDebug from "debug";
+import { GenericBar } from "cli-progress";
const debug = createDebug("refresh:actions:ResetLdap");
export default class ResetLdap implements ActionInterface {
async prompt(): Promise {
- const response = await prompts({
+ const { shouldRefresh } = await prompts({
type: "confirm",
- name: "ldap-refresh",
+ name: "shouldRefresh",
message: "Do you want to reset the LDAP userpool?",
});
- return response["ldap-refresh"];
+ return shouldRefresh;
}
- async execute(): Promise {
- debug(chalk.greenBright(`Deleting existing users`));
+ async execute(progress: GenericBar): Promise {
+ progress.start(3, 0);
+
+ debug(chalk.greenBright(`Deleting existing users from LDAP`));
const searchLdap = `ldapsearch -D "cn=admin,dc=vol,dc=dvsa" -H ldap://localhost:1389 -w admin -LLL -s one -b "ou=users,dc=vol,dc=dvsa" "(cn=*)" dn`;
- const search = shell.exec(
+ const search = exec(
`docker compose exec -T openldap /bin/bash -c '${searchLdap}' | awk '/^dn: / {print $2}'`,
- {
- silent: !debug.enabled,
- },
+ debug,
);
+ progress.increment();
+
const allExistingUsers = search.stdout.split("\n").filter(Boolean);
const deleteLdif = allExistingUsers.map((dn) => `dn: ${dn}\nchangetype: delete`).join("\n\n");
@@ -37,23 +40,25 @@ export default class ResetLdap implements ActionInterface {
${deleteLdif}
!`;
- const deleteUsers = shell.exec(`docker compose exec openldap /bin/bash -c "${ldifDeletions}"`);
+ const deleteUsers = exec(`docker compose exec openldap /bin/bash -c "${ldifDeletions}"`, debug);
if (deleteUsers.code !== 0) {
- console.error(chalk.red(`Error while deleting users from LDAP`));
- return;
+ throw new Error("Delete users from LDAP failed");
}
- const selectAllUsersCmd = shell.exec(
+ progress.increment();
+
+ const selectAllUsersCmd = exec(
`docker compose exec db /bin/bash -c 'mysql -u mysql -polcs -N -e "SELECT login_id FROM olcs_be.user WHERE login_id IS NOT NULL"'`,
- { silent: !debug.enabled, env: { ...process.env, FORCE_COLOR: "1" } },
+ debug,
);
if (selectAllUsersCmd.code !== 0) {
- console.error(chalk.red(`Error while selecting users from database`));
- return;
+ throw new Error("Select users from database failed");
}
+ progress.increment();
+
const allUsers = selectAllUsersCmd.stdout.split("\n").filter(Boolean);
const ldif = allUsers
@@ -67,22 +72,13 @@ export default class ResetLdap implements ActionInterface {
)
.join("\n\n");
- debug(chalk.greenBright(`Adding new users`));
+ debug(`Adding new users into LDAP`);
const ldifModify = dedent`ldapmodify -D "cn=admin,dc=vol,dc=dvsa" -H ldap://localhost:1389 -w admin -c < {
+ const optionsWithDefaults = {
+ silent: true,
+ env: {
+ ...process.env,
+ FORCE_COLOR: "1",
+ },
+ ...options,
+ };
+
+ const result = shell.exec(command, optionsWithDefaults);
+
+ if (result.stdout && debug.enabled) {
+ debug(result.stdout);
+ }
+
+ if (result.stderr) {
+ debug(result.stderr);
+ }
+
+ if (result.code !== 0) {
+ throw new Error(`Command: ${command} failed. Stderr: ${result.stderr}`);
+ }
+
+ return result;
+};
+
+export default exec;
diff --git a/packages/local-refresh/index.ts b/packages/local-refresh/src/index.ts
similarity index 68%
rename from packages/local-refresh/index.ts
rename to packages/local-refresh/src/index.ts
index ff504897fb..e40bc29d50 100644
--- a/packages/local-refresh/index.ts
+++ b/packages/local-refresh/src/index.ts
@@ -2,11 +2,21 @@
import { program } from "commander";
import fs from "node:fs";
-import path from "path";
+import path from "node:path";
import chalk from "chalk";
+import cliProgress from "cli-progress";
import ActionInterface from "./actions/ActionInterface";
-program.description("Reset the VOL local environment.").action(async () => {
+const progressBarFactory = () => {
+ return new cliProgress.Bar(
+ {
+ clearOnComplete: true,
+ },
+ cliProgress.Presets.shades_classic,
+ );
+};
+
+program.description("Script to refresh the local VOL application").action(async () => {
const actions = await Promise.all(
fs
.readdirSync(path.resolve(__dirname, "actions"))
@@ -29,29 +39,34 @@ program.description("Reset the VOL local environment.").action(async () => {
const shouldRun = await instance.prompt();
if (shouldRun) {
- console.info(`Running action: ${instance.constructor.name}`);
- await instance.execute();
+ try {
+ await instance.execute(progressBarFactory());
+ } catch (e: unknown) {
+ if (e instanceof Error) {
+ console.error(`\n\n${chalk.red(e.message)}\n`);
+ }
+ }
}
}
const hostsFile = fs.readFileSync("/etc/hosts", "utf8");
if (!hostsFile.includes("local.olcs.dev-dvsacloud.uk")) {
- console.warn(chalk.yellow(`/etc/hosts has not been updated with local domains. Please run:`));
+ console.warn(chalk.yellow(`/etc/hosts has not been updated with the local domains. Please run:`));
console.warn(
chalk.bgYellow(
`sudo echo "127.0.0.1 iuweb.local.olcs.dev-dvsacloud.uk ssweb.local.olcs.dev-dvsacloud.uk api.local.olcs.dev-dvsacloud.uk cdn.local.olcs.dev-dvsacloud.uk" >> /etc/hosts`,
),
);
-
- return;
}
- console.info(chalk.greenBright("Local environment reset complete."));
+ process.exit(0);
});
program.parse(process.argv);
process.on("unhandledRejection", (err) => {
+ console.error(`\n\nUncaught Error: ${chalk.red(err)}\n\n`);
+
process.exit(1);
});