Compare commits

..

32 Commits

Author SHA1 Message Date
semantic-release-bot
f63e507ddf chore(release): 7.6.0 [skip ci]
# [7.6.0](https://git.datacontroller.io/dc/dc/compare/v7.5.0...v7.6.0) (2026-04-03)

### Bug Fixes

* add label and tooltip for libref download, sanitise input ([52d5803](52d58036a4))

### Features

* configurable email alerts.  Closes [#217](#217) ([2ccf0d1](2ccf0d1100))
2026-04-03 22:17:14 +00:00
991cc0567d Merge pull request 'feat: configurable email alerts. Closes #217' (#222) from issue217 into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 3m42s
Release / Build-and-test-development (push) Successful in 10m10s
Release / release (push) Successful in 7m48s
Reviewed-on: #222
2026-04-03 21:09:11 +00:00
s
52d58036a4 fix: add label and tooltip for libref download, sanitise input
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m6s
Build / Build-and-test-development (pull_request) Successful in 10m13s
Lighthouse Checks / lighthouse (pull_request) Successful in 18m37s
2026-04-03 19:55:42 +02:00
26bff85792 chore: fix debug line
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m47s
Build / Build-and-test-development (pull_request) Successful in 10m16s
Lighthouse Checks / lighthouse (pull_request) Successful in 19m41s
2026-04-03 18:35:48 +01:00
2ccf0d1100 feat: configurable email alerts. Closes #217
Some checks failed
Build / Build-and-ng-test (pull_request) Successful in 4m42s
Build / Build-and-test-development (pull_request) Has been cancelled
Lighthouse Checks / lighthouse (pull_request) Has been cancelled
2026-04-03 18:34:23 +01:00
semantic-release-bot
3be33186bc chore(release): 7.5.0 [skip ci]
# [7.5.0](https://git.datacontroller.io/dc/dc/compare/v7.4.1...v7.5.0) (2026-04-03)

### Bug Fixes

* add workflow audits, update deps ([66e98a9](66e98a96cb))
* allow CSV uploads with licence row limit ([5b260e4](5b260e4915)), closes [#213](#213)
* bumping cli and pinning versions in .npmrc ([80039f4](80039f4876))
* guard CSV upload with fileUpload licence flag ([ed40df6](ed40df6295))
* parse embed param from window.location.hash for hash router compatibility ([0269c24](0269c2421d))
* quote CSV char values.  Closes [#215](#215) ([d9980e8](d9980e866d))
* resolve outer promise in parseCsvFile for non-WLATIN1 path ([4ee15e1](4ee15e1b6e))
* use XLSX for CSV row truncation to handle new lines in values ([6d590c0](6d590c050d))

### Features

* add embed URL parameter to hide header and back button ([b0dc441](b0dc441d68)), closes [#214](#214)
* add target libref input to config download ([a89657b](a89657b0b8)), closes [#212](#212)
* export config service to allow dclib swapping.  Closes [#212](#212) ([326c26f](326c26fddf))
2026-04-03 11:06:36 +00:00
1a7f950ae2 Merge pull request 'feat: enabling dclib switching when exporting config' (#220) from issue212 into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 3m39s
Release / Build-and-test-development (push) Successful in 9m55s
Release / release (push) Successful in 7m46s
Reviewed-on: #220
2026-04-03 10:49:43 +00:00
8924dc8ab1 chore: merge buid.yaml
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m58s
Build / Build-and-test-development (pull_request) Successful in 10m3s
Lighthouse Checks / lighthouse (pull_request) Successful in 18m46s
2026-04-03 10:30:05 +00:00
s
2c2901b537 chore: rever upload artifacts actions version
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m3s
Build / Build-and-test-development (pull_request) Successful in 10m0s
Lighthouse Checks / lighthouse (pull_request) Successful in 18m24s
2026-04-03 10:32:28 +02:00
s
2cae7ea638 chore: improve CI workflows
Some checks failed
Build / Build-and-ng-test (pull_request) Successful in 4m3s
Build / Build-and-test-development (pull_request) Failing after 10m17s
Lighthouse Checks / lighthouse (pull_request) Failing after 18m31s
2026-04-03 09:36:39 +02:00
s
66e98a96cb fix: add workflow audits, update deps
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m2s
Build / Build-and-test-development (pull_request) Successful in 10m19s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m53s
2026-04-03 09:10:49 +02:00
0b0db1c543 chore: run audit check in build.yaml as well as release.yaml
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 1m31s
Build / Build-and-test-development (pull_request) Successful in 10m23s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m32s
2026-04-03 01:18:54 +00:00
80039f4876 fix: bumping cli and pinning versions in .npmrc
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m51s
Build / Build-and-test-development (pull_request) Successful in 10m9s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m11s
2026-04-03 02:02:05 +01:00
326c26fddf feat: export config service to allow dclib swapping. Closes #212 2026-04-03 02:01:44 +01:00
e7b2ead0e2 Merge pull request 'fix: allow CSV uploads with licence row limit' (#219) from fix/213-csv-license-row-limit into main
Some checks failed
Release / Build-production-and-ng-test (push) Failing after 1m25s
Release / Build-and-test-development (push) Has been skipped
Release / release (push) Has been skipped
Reviewed-on: #219
2026-04-02 19:08:18 +00:00
s
a89657b0b8 feat: add target libref input to config download
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m5s
Build / Build-and-test-development (pull_request) Successful in 10m16s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m30s
Closes #212
2026-04-02 19:37:55 +02:00
s
4ee15e1b6e fix: resolve outer promise in parseCsvFile for non-WLATIN1 path
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m55s
Build / Build-and-test-development (pull_request) Successful in 10m21s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m49s
2026-04-02 18:48:27 +02:00
s
ed40df6295 fix: guard CSV upload with fileUpload licence flag
Some checks failed
Build / Build-and-ng-test (pull_request) Successful in 4m3s
Build / Build-and-test-development (pull_request) Failing after 11m54s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m35s
2026-04-02 17:40:16 +02:00
s
6d590c050d fix: use XLSX for CSV row truncation to handle new lines in values
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m53s
Build / Build-and-test-development (pull_request) Successful in 10m25s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m29s
2026-04-02 17:03:16 +02:00
47f9a54f97 Merge pull request 'feat: add embed URL parameter to hide header and back button' (#218) from feat/214-hide-titlebar-embed into fix/213-csv-license-row-limit
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m0s
Build / Build-and-test-development (pull_request) Successful in 10m18s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m56s
Reviewed-on: #218
2026-04-02 14:37:06 +00:00
s
17b0d72fbf test: add csv-limited spec to cypress workflow
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m1s
Build / Build-and-test-development (pull_request) Successful in 10m23s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m1s
2026-04-02 16:13:35 +02:00
s
0269c2421d fix: parse embed param from window.location.hash for hash router compatibility
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m9s
Build / Build-and-test-development (pull_request) Successful in 10m9s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m8s
2026-04-02 14:57:16 +02:00
s
5b260e4915 fix: allow CSV uploads with licence row limit
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m56s
Build / Build-and-test-development (pull_request) Successful in 10m3s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m49s
Fixes #213
2026-04-02 14:34:58 +02:00
5290410a17 Merge branch 'main' into feat/214-hide-titlebar-embed
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m56s
Build / Build-and-test-development (pull_request) Successful in 10m11s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m31s
2026-04-02 11:13:49 +00:00
dc9041aaec Merge pull request 'fix: quote CSV char values. Closes #215' (#216) from issue215 into main
Some checks failed
Release / Build-production-and-ng-test (push) Failing after 1m25s
Release / Build-and-test-development (push) Has been skipped
Release / release (push) Has been skipped
Reviewed-on: #216
2026-04-02 11:12:38 +00:00
s
b0dc441d68 feat: add embed URL parameter to hide header and back button
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m3s
Build / Build-and-test-development (pull_request) Successful in 10m6s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m39s
Closes #214
2026-04-02 11:26:28 +02:00
b0fc3eb5af chore: update comment
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m23s
Build / Build-and-test-development (pull_request) Successful in 10m7s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m13s
2026-03-31 17:09:17 +01:00
d9980e866d fix: quote CSV char values. Closes #215
Some checks failed
Build / Build-and-ng-test (pull_request) Successful in 4m7s
Build / Build-and-test-development (pull_request) Has been cancelled
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Has been cancelled
2026-03-31 17:04:46 +01:00
semantic-release-bot
52ae3404ee chore(release): 7.4.1 [skip ci]
## [7.4.1](https://git.datacontroller.io/dc/dc/compare/v7.4.0...v7.4.1) (2026-03-12)

### Bug Fixes

* support for SASIOSNF engine (SNOW alias) plus meta assignment ([7694d1b](7694d1b0fb))
2026-03-12 00:52:17 +00:00
eecb4f4f53 Merge pull request 'fix: support for SASIOSNF engine (SNOW alias) plus meta assignment' (#209) from snowfixes into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 3m41s
Release / Build-and-test-development (push) Successful in 9m52s
Release / release (push) Successful in 7m57s
Reviewed-on: #209
2026-03-12 00:35:16 +00:00
744345af81 chore: bump sasjs/cli
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m51s
Build / Build-and-test-development (pull_request) Successful in 9m58s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m48s
2026-03-12 00:16:10 +00:00
_
7694d1b0fb fix: support for SASIOSNF engine (SNOW alias) plus meta assignment
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m46s
Build / Build-and-test-development (pull_request) Successful in 9m38s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m18s
2026-03-10 23:50:57 +00:00
33 changed files with 849 additions and 335 deletions

View File

@@ -2,32 +2,31 @@ name: Build
run-name: Running Lint Check and Licence checker on Pull Request run-name: Running Lint Check and Licence checker on Pull Request
on: [pull_request] on: [pull_request]
env:
NODE_VERSION: '24.5.0'
jobs: jobs:
Build-and-ng-test: Build-and-ng-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 24.5.0 node-version: ${{ env.NODE_VERSION }}
- name: Install Google Chrome - name: Install Google Chrome
run: | run: |
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list
apt-get update apt-get update
apt-get install -y google-chrome-stable xvfb wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
apt install -y ./google-chrome*.deb
- name: Write .npmrc file - name: Write .npmrc file
run: echo "$NPMRC" > client/.npmrc run: echo "$NPMRC" >> client/.npmrc
shell: bash shell: bash
env: env:
NPMRC: ${{ secrets.NPMRC}} NPMRC: ${{ secrets.NPMRC}}
- name: Lint check
run: npm run lint:check
- name: Install dependencies - name: Install dependencies
run: | run: |
cd client cd client
@@ -35,6 +34,18 @@ jobs:
echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
npm ci npm ci
- name: Check audit
# Audit should fail and stop the CI if critical vulnerability found
run: |
npm audit --audit-level=critical --omit=dev
cd ./sas
npm audit --audit-level=critical --omit=dev
cd ../client
npm audit --audit-level=critical --omit=dev
- name: Lint check
run: npm run lint:check
- name: Licence checker - name: Licence checker
run: | run: |
cd client cd client
@@ -52,26 +63,27 @@ jobs:
Build-and-test-development: Build-and-test-development:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: Build-production-and-ng-test needs: Build-and-ng-test
env:
CHROME_BIN: /usr/bin/google-chrome
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 24.5.0 node-version: ${{ env.NODE_VERSION }}
- name: Write .npmrc file - name: Write .npmrc file
run: | run: |
touch client/.npmrc touch client/.npmrc
echo '${{ secrets.NPMRC}}' > client/.npmrc echo '${{ secrets.NPMRC}}' > client/.npmrc
- run: apt-get update - name: Install system dependencies
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb run: |
- run: apt install -y ./google-chrome*.deb; apt-get update
- run: export CHROME_BIN=/usr/bin/google-chrome wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
- run: apt-get update -y apt install -y ./google-chrome*.deb
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb jq zip
- run: apt -y install jq
- name: Write cypress credentials - name: Write cypress credentials
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
@@ -86,17 +98,18 @@ jobs:
echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
npm ci npm ci
# Install pm2 and prepare SASJS server - name: Setup and start SASjs server
- run: npm i -g pm2 run: |
- run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip npm i -g pm2
- run: unzip linux.zip curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
- run: touch .env unzip linux.zip
- run: echo RUN_TIMES=js >> .env touch .env
- run: echo NODE_PATH=node >> .env echo RUN_TIMES=js >> .env
- run: echo CORS=enable >> .env echo NODE_PATH=node >> .env
- run: echo WHITELIST=http://localhost:4200 >> .env echo CORS=enable >> .env
- run: cat .env echo WHITELIST=http://localhost:4200 >> .env
- run: pm2 start api-linux --wait-ready cat .env
pm2 start api-linux --wait-ready
- name: Deploy mocked services - name: Deploy mocked services
run: | run: |
@@ -106,11 +119,6 @@ jobs:
sasjs cbd -t server-ci sasjs cbd -t server-ci
# sasjs request services/admin/makedata -t server-ci -d ./deploy/makeData4GL.json -c ./deploy/requestConfig.json -o ./output.json # sasjs request services/admin/makedata -t server-ci -d ./deploy/makeData4GL.json -c ./deploy/requestConfig.json -o ./output.json
- name: Install ZIP
run: |
apt-get update
apt-get install zip
- name: Prepare and run frontend and cypress - name: Prepare and run frontend and cypress
run: | run: |
cd ./client cd ./client
@@ -126,7 +134,7 @@ jobs:
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
cat ./cypress.config.ts cat ./cypress.config.ts
# Start frontend and run cypress # Start frontend and run cypress
npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts" npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/csv-limited.cy.ts,cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts"
- name: Zip Cypress videos - name: Zip Cypress videos
if: always() if: always()

View File

@@ -2,38 +2,31 @@ name: Lighthouse Checks
run-name: Running Lighthouse Performance and Accessibility Checks on Pull Request run-name: Running Lighthouse Performance and Accessibility Checks on Pull Request
on: [pull_request] on: [pull_request]
env:
NODE_VERSION: '24.5.0'
jobs: jobs:
lighthouse: lighthouse:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node-version: [24.5.0]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ env.NODE_VERSION }}
- name: Install Google Chrome - name: Install Google Chrome
run: | run: |
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list
apt-get update apt-get update
apt-get install -y google-chrome-stable xvfb wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
apt install -y ./google-chrome*.deb
- name: Install pm2 for process management - name: Install global packages
run: npm i -g pm2 run: npm i -g pm2 @sasjs/cli wait-on
- name: Install @sasjs/cli - name: Setup and start SASjs server
run: npm i -g @sasjs/cli
- name: Install wait-on globally
run: npm install -g wait-on
- name: Create .env file for sasjs/server
run: | run: |
touch .env touch .env
echo RUN_TIMES=js >> .env echo RUN_TIMES=js >> .env
@@ -41,15 +34,9 @@ jobs:
echo CORS=enable >> .env echo CORS=enable >> .env
echo WHITELIST=http://localhost:4200 >> .env echo WHITELIST=http://localhost:4200 >> .env
cat .env cat .env
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
- name: Download sasjs/server package from github using curl unzip linux.zip
run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip pm2 start api-linux --wait-ready
- name: Unzip downloaded package
run: unzip linux.zip
- name: Run sasjs server
run: pm2 start api-linux --wait-ready
- name: Write .npmrc file - name: Write .npmrc file
run: echo "$NPMRC" > client/.npmrc run: echo "$NPMRC" > client/.npmrc

View File

@@ -5,15 +5,20 @@ on:
branches: branches:
- main - main
env:
NODE_VERSION: '24.5.0'
jobs: jobs:
Build-production-and-ng-test: Build-production-and-ng-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
CHROME_BIN: /usr/bin/google-chrome
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 24.5.0 node-version: ${{ env.NODE_VERSION }}
- name: Write .npmrc file - name: Write .npmrc file
run: | run: |
@@ -24,8 +29,7 @@ jobs:
run: | run: |
apt-get update apt-get update
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
apt install -y ./google-chrome*.deb; apt install -y ./google-chrome*.deb
export CHROME_BIN=/usr/bin/google-chrome
- name: Write cypress credentials - name: Write cypress credentials
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
@@ -43,9 +47,9 @@ jobs:
- name: Check audit - name: Check audit
# Audit should fail and stop the CI if critical vulnerability found # Audit should fail and stop the CI if critical vulnerability found
run: | run: |
npm audit --audit-level=critical --omit=dev npm audit --omit=dev
cd ./sas cd ./sas
npm audit --audit-level=critical --omit=dev npm audit --omit=dev
cd ../client cd ../client
npm audit --audit-level=critical --omit=dev npm audit --audit-level=critical --omit=dev
@@ -63,25 +67,26 @@ jobs:
Build-and-test-development: Build-and-test-development:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: Build-production-and-ng-test needs: Build-production-and-ng-test
env:
CHROME_BIN: /usr/bin/google-chrome
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 24.5.0 node-version: ${{ env.NODE_VERSION }}
- name: Write .npmrc file - name: Write .npmrc file
run: | run: |
touch client/.npmrc touch client/.npmrc
echo '${{ secrets.NPMRC}}' > client/.npmrc echo '${{ secrets.NPMRC}}' > client/.npmrc
- run: apt-get update - name: Install system dependencies
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb run: |
- run: apt install -y ./google-chrome*.deb; apt-get update
- run: export CHROME_BIN=/usr/bin/google-chrome wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
- run: apt-get update -y apt install -y ./google-chrome*.deb
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb jq zip
- run: apt -y install jq
- name: Write cypress credentials - name: Write cypress credentials
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
@@ -96,17 +101,18 @@ jobs:
echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
npm ci npm ci
# Install pm2 and prepare SASJS server - name: Setup and start SASjs server
- run: npm i -g pm2 run: |
- run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip npm i -g pm2
- run: unzip linux.zip curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
- run: touch .env unzip linux.zip
- run: echo RUN_TIMES=js >> .env touch .env
- run: echo NODE_PATH=node >> .env echo RUN_TIMES=js >> .env
- run: echo CORS=enable >> .env echo NODE_PATH=node >> .env
- run: echo WHITELIST=http://localhost:4200 >> .env echo CORS=enable >> .env
- run: cat .env echo WHITELIST=http://localhost:4200 >> .env
- run: pm2 start api-linux --wait-ready cat .env
pm2 start api-linux --wait-ready
- name: Deploy mocked services - name: Deploy mocked services
run: | run: |
@@ -116,11 +122,6 @@ jobs:
sasjs cbd -t server-ci sasjs cbd -t server-ci
# sasjs request services/admin/makedata -t server-ci -d ./deploy/makeData4GL.json -c ./deploy/requestConfig.json -o ./output.json # sasjs request services/admin/makedata -t server-ci -d ./deploy/makeData4GL.json -c ./deploy/requestConfig.json -o ./output.json
- name: Install ZIP
run: |
apt-get update
apt-get install zip
- name: Prepare and run frontend and cypress - name: Prepare and run frontend and cypress
run: | run: |
cd ./client cd ./client
@@ -136,7 +137,7 @@ jobs:
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
cat ./cypress.config.ts cat ./cypress.config.ts
# Start frontend and run cypress # Start frontend and run cypress
npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts" npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/csv-limited.cy.ts,cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts"
- name: Zip Cypress videos - name: Zip Cypress videos
if: always() if: always()
@@ -155,10 +156,10 @@ jobs:
needs: [Build-production-and-ng-test, Build-and-test-development] needs: [Build-production-and-ng-test, Build-and-test-development]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 24.5.0 node-version: ${{ env.NODE_VERSION }}
- name: Write .npmrc file - name: Write .npmrc file
run: | run: |
@@ -168,17 +169,11 @@ jobs:
env: env:
NPMRC: ${{ secrets.NPMRC}} NPMRC: ${{ secrets.NPMRC}}
- name: Install packages - name: Install system packages
run: | run: |
apt-get update apt-get update
apt-get install zip -y apt-get install -y zip jq doxygen
# sasjs cli is used to compile & build the SAS services
npm i -g @sasjs/cli npm i -g @sasjs/cli
# jq is used to parse the release JSON
apt-get install jq -y
# doxygen is used for the SASJS docs
apt-get update
apt-get install doxygen -y
- name: Frontend Preliminary Build - name: Frontend Preliminary Build
description: We want to prevent creating empty release if frontend fails description: We want to prevent creating empty release if frontend fails

2
.npmrc
View File

@@ -1 +1,3 @@
legacy-peer-deps=true legacy-peer-deps=true
ignore-scripts=true
save-exact=true

View File

@@ -1,3 +1,43 @@
# [7.6.0](https://git.datacontroller.io/dc/dc/compare/v7.5.0...v7.6.0) (2026-04-03)
### Bug Fixes
* add label and tooltip for libref download, sanitise input ([52d5803](https://git.datacontroller.io/dc/dc/commit/52d58036a40e25847e900f9b04a77dbcc409c12b))
### Features
* configurable email alerts. Closes [#217](https://git.datacontroller.io/dc/dc/issues/217) ([2ccf0d1](https://git.datacontroller.io/dc/dc/commit/2ccf0d11000129629a0665421135b7530af9892f))
# [7.5.0](https://git.datacontroller.io/dc/dc/compare/v7.4.1...v7.5.0) (2026-04-03)
### Bug Fixes
* add workflow audits, update deps ([66e98a9](https://git.datacontroller.io/dc/dc/commit/66e98a96cbd092e762b94a04660f8e17ca003ceb))
* allow CSV uploads with licence row limit ([5b260e4](https://git.datacontroller.io/dc/dc/commit/5b260e49153dd85bc0023ad94d8a5f57b8ffa6dc)), closes [#213](https://git.datacontroller.io/dc/dc/issues/213)
* bumping cli and pinning versions in .npmrc ([80039f4](https://git.datacontroller.io/dc/dc/commit/80039f4876c8e09dc477678e1eff58329094c9e9))
* guard CSV upload with fileUpload licence flag ([ed40df6](https://git.datacontroller.io/dc/dc/commit/ed40df62953c3055770b5cbf50738f4a48b943cd))
* parse embed param from window.location.hash for hash router compatibility ([0269c24](https://git.datacontroller.io/dc/dc/commit/0269c2421db245f7f5405678605cb4d4587e2a67))
* quote CSV char values. Closes [#215](https://git.datacontroller.io/dc/dc/issues/215) ([d9980e8](https://git.datacontroller.io/dc/dc/commit/d9980e866d1a2fe7a731ff279d73accd35003e67))
* resolve outer promise in parseCsvFile for non-WLATIN1 path ([4ee15e1](https://git.datacontroller.io/dc/dc/commit/4ee15e1b6e83f27f279fc345e6998452a8f64d7e))
* use XLSX for CSV row truncation to handle new lines in values ([6d590c0](https://git.datacontroller.io/dc/dc/commit/6d590c050dcd593a73464fae5604f774f016b10d))
### Features
* add embed URL parameter to hide header and back button ([b0dc441](https://git.datacontroller.io/dc/dc/commit/b0dc441d681369e06eee58288dbdbb236f930bdc)), closes [#214](https://git.datacontroller.io/dc/dc/issues/214)
* add target libref input to config download ([a89657b](https://git.datacontroller.io/dc/dc/commit/a89657b0b81b9c531f64c0dda2714b4eb16c4bc9)), closes [#212](https://git.datacontroller.io/dc/dc/issues/212)
* export config service to allow dclib swapping. Closes [#212](https://git.datacontroller.io/dc/dc/issues/212) ([326c26f](https://git.datacontroller.io/dc/dc/commit/326c26fddfa88a0dc4ca79d3bd0c77c4d807f37c))
## [7.4.1](https://git.datacontroller.io/dc/dc/compare/v7.4.0...v7.4.1) (2026-03-12)
### Bug Fixes
* support for SASIOSNF engine (SNOW alias) plus meta assignment ([7694d1b](https://git.datacontroller.io/dc/dc/commit/7694d1b0fb2bd0407c8598147fbae87a00d889a8))
# [7.4.0](https://git.datacontroller.io/dc/dc/compare/v7.3.0...v7.4.0) (2026-02-20) # [7.4.0](https://git.datacontroller.io/dc/dc/compare/v7.3.0...v7.4.0) (2026-02-20)

View File

@@ -0,0 +1,95 @@
const username = Cypress.env('username')
const password = Cypress.env('password')
const hostUrl = Cypress.env('hosturl')
const appLocation = Cypress.env('appLocation')
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
const serverType = Cypress.env('serverType')
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
const fixturePath = 'csvs/'
context('csv file upload restriction (free tier): ', function () {
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
cy.get('body').then(($body) => {
const usernameInput = $body.find('input.username')[0]
if (usernameInput && !Cypress.dom.isHidden(usernameInput)) {
cy.get('input.username').type(username)
cy.get('input.password').type(password)
cy.get('.login-group button').click()
}
})
cy.get('.app-loading', { timeout: longerCommandTimeout }).should(
'not.exist'
)
// Skip licensing page if presented - continue with free tier
cy.url().then((url) => {
if (url.includes('licensing')) {
cy.get('button').contains('Continue with free tier').click()
}
})
visitPage('home')
})
it('1 | File upload is restricted on free tier', () => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
// Click upload button - should show feature locked modal
cy.get('.buttonBar button:last-child').should('exist').click()
cy.get('.modal-title').should('contain', 'Locked Feature (File Upload)')
})
})
const openTableFromTree = (libNameIncludes: string, tablename: string) => {
cy.get('.app-loading', { timeout: longerCommandTimeout })
.should('not.exist')
.then(() => {
cy.get('.nav-tree clr-tree > clr-tree-node', {
timeout: longerCommandTimeout
}).then((treeNodes: any) => {
let targetLib
for (let node of treeNodes) {
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
targetLib = node
break
}
}
cy.get(targetLib).within(() => {
cy.get('.clr-tree-node-content-container > button').click()
cy.get('.clr-treenode-link').then((innerNodes: any) => {
for (let innerNode of innerNodes) {
if (innerNode.innerText.toLowerCase().includes(tablename)) {
innerNode.click()
break
}
}
})
})
})
})
}
const attachFile = (filename: string, callback?: any) => {
cy.get('.buttonBar button:last-child')
.should('exist')
.click()
.then(() => {
cy.get('input[type="file"]#file-upload')
.attachFile(`/${fixturePath}/${filename}`)
.then(() => {
if (callback) callback()
})
})
}
const visitPage = (url: string) => {
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
}

View File

@@ -4,7 +4,11 @@ PRIMARY_KEY_FIELD,SOME_CHAR,SOME_DROPDOWN,SOME_NUM,SOME_DATE,SOME_DATETIME,SOME_
2,even more dummy data,Option 3,42,12FEB1960,01JAN1960:00:00:42,0:02:22,3,44 2,even more dummy data,Option 3,42,12FEB1960,01JAN1960:00:00:42,0:02:22,3,44
3,"It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told:",Option 2,1613.001,27FEB1961,01JAN1960:00:07:03,0:00:44,3,44 3,"It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told:",Option 2,1613.001,27FEB1961,01JAN1960:00:07:03,0:00:44,3,44
4,if you can fill the unforgiving minute,Option 1,1613.0011235,02AUG1971,29MAY1973:06:12:03,0:06:52,3,44 4,if you can fill the unforgiving minute,Option 1,1613.0011235,02AUG1971,29MAY1973:06:12:03,0:06:52,3,44
1010,10 bottles of beer on the wall,Option 1,0.9153696885,04MAR1962,01JAN1960:12:47:55,0:01:40,92,76 1010,"10 bottles of beer
on the wall",Option 1,0.9153696885,04MAR1962,01JAN1960:12:47:55,0:01:40,92,76
1011,11 bottles of beer on the wall,Option 1,0.3531217558,29MAR1960,01JAN1960:03:33:24,0:01:03,80,29 1011,11 bottles of beer on the wall,Option 1,0.3531217558,29MAR1960,01JAN1960:03:33:24,0:01:03,80,29
1012,12 bottles of beer on the wall,Option 1,0.6743748717,02AUG1962,01JAN1960:07:25:59,0:00:10,16,98 1012,12 bottles of beer on the wall,Option 1,0.6743748717,02AUG1962,01JAN1960:07:25:59,0:00:10,16,98
1013,13 bottles of beer on the wall,Option 1,0.1305445992,11SEP1960,01JAN1960:13:51:32,0:00:35,73,15 1013,13 bottles of beer on the wall,Option 1,0.1305445992,11SEP1960,01JAN1960:13:51:32,0:00:35,73,15
1 PRIMARY_KEY_FIELD SOME_CHAR SOME_DROPDOWN SOME_NUM SOME_DATE SOME_DATETIME SOME_TIME SOME_SHORTNUM SOME_BESTNUM
4 2 even more dummy data Option 3 42 12FEB1960 01JAN1960:00:00:42 0:02:22 3 44
5 3 It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: Option 2 1613.001 27FEB1961 01JAN1960:00:07:03 0:00:44 3 44
6 4 if you can fill the unforgiving minute Option 1 1613.0011235 02AUG1971 29MAY1973:06:12:03 0:06:52 3 44
7 1010 10 bottles of beer on the wall 10 bottles of beer on the wall Option 1 0.9153696885 04MAR1962 01JAN1960:12:47:55 0:01:40 92 76
8 1011 11 bottles of beer on the wall Option 1 0.3531217558 29MAR1960 01JAN1960:03:33:24 0:01:03 80 29
9 1012 12 bottles of beer on the wall Option 1 0.6743748717 02AUG1962 01JAN1960:07:25:59 0:00:10 16 98
10 1013 13 bottles of beer on the wall Option 1 0.1305445992 11SEP1960 01JAN1960:13:51:32 0:00:35 73 15
11 1014 14 bottles of beer on the wall Option 1 0.7409067949 26JUL1960 01JAN1960:05:18:10 0:00:41 30 89
12 1011 1015 11 bottles of beer on the wall 15 bottles of beer on the wall Option 1 0.3531217558 0.0869016028 29MAR1960 28FEB1961 01JAN1960:03:33:24 01JAN1960:13:23:45 0:01:03 0:00:44 80 29 3
13 1012 1016 12 bottles of beer on the wall 16 bottles of beer on the wall Option 1 0.6743748717 0.0462121419 02AUG1962 09AUG1962 01JAN1960:07:25:59 01JAN1960:07:42:38 0:00:10 0:01:17 16 62 98 2
14 1013 1017 13 bottles of beer on the wall 17 bottles of beer on the wall Option 1 0.1305445992 0.7501918947 11SEP1960 14MAY1962 01JAN1960:13:51:32 01JAN1960:04:40:20 0:00:35 0:00:15 73 53 15 65

View File

@@ -37,7 +37,7 @@
"hyperformula": "^2.5.0", "hyperformula": "^2.5.0",
"iconv-lite": "^0.5.0", "iconv-lite": "^0.5.0",
"jquery-datetimepicker": "^2.5.21", "jquery-datetimepicker": "^2.5.21",
"jsrsasign": "^11.1.0", "jsrsasign": "11.1.1",
"marked": "^5.0.0", "marked": "^5.0.0",
"moment": "^2.30.1", "moment": "^2.30.1",
"ngx-clipboard": "^16.0.0", "ngx-clipboard": "^16.0.0",
@@ -18234,9 +18234,9 @@
} }
}, },
"node_modules/jsrsasign": { "node_modules/jsrsasign": {
"version": "11.1.0", "version": "11.1.1",
"resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-11.1.0.tgz", "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-11.1.1.tgz",
"integrity": "sha512-Ov74K9GihaK9/9WncTe1mPmvrO7Py665TUfUKvraXBpu+xcTWitrtuOwcjf4KMU9maPaYn0OuaWy0HOzy/GBXg==", "integrity": "sha512-6w95OOXH8DNeGxakqLndBEqqwQ6A70zGaky1oxfg8WVLWOnghTfJsc5Tknx+Z88MHSb1bGLcqQHImOF8Lk22XA==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/kjur/jsrsasign#donations" "url": "https://github.com/kjur/jsrsasign#donations"
@@ -25444,9 +25444,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "7.16.0", "version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true "optional": true

View File

@@ -67,7 +67,7 @@
"hyperformula": "^2.5.0", "hyperformula": "^2.5.0",
"iconv-lite": "^0.5.0", "iconv-lite": "^0.5.0",
"jquery-datetimepicker": "^2.5.21", "jquery-datetimepicker": "^2.5.21",
"jsrsasign": "^11.1.0", "jsrsasign": "11.1.1",
"marked": "^5.0.0", "marked": "^5.0.0",
"moment": "^2.30.1", "moment": "^2.30.1",
"ngx-clipboard": "^16.0.0", "ngx-clipboard": "^16.0.0",

View File

@@ -55,6 +55,7 @@ export interface HandsontableStaticConfig {
* Cached viyaApi collections, search and selected endpoint * Cached viyaApi collections, search and selected endpoint
*/ */
export const globals: { export const globals: {
embed: boolean
rootParam: string rootParam: string
dcLib: string dcLib: string
xlmaps: XLMapListItem[] xlmaps: XLMapListItem[]
@@ -69,6 +70,7 @@ export const globals: {
handsontable: HandsontableStaticConfig handsontable: HandsontableStaticConfig
[key: string]: any [key: string]: any
} = { } = {
embed: false,
rootParam: <string>'', rootParam: <string>'',
dcLib: '', dcLib: '',
xlmaps: [], xlmaps: [],

View File

@@ -107,7 +107,7 @@
</div> </div>
</ng-container> </ng-container>
<header class="app-header"> <header class="app-header" *ngIf="!embed">
<!-- <button <!-- <button
*ngIf=" *ngIf="
isMainRoute('view') || isMainRoute('view') ||
@@ -213,9 +213,10 @@
</header> </header>
<nav <nav
*ngIf=" *ngIf="
router.url.includes('submitted') || !embed &&
router.url.includes('approve') || (router.url.includes('submitted') ||
router.url.includes('history') router.url.includes('approve') ||
router.url.includes('history'))
" "
class="subnav" class="subnav"
> >

View File

@@ -70,6 +70,7 @@ export class AppComponent {
public syssite = this.appService.syssite public syssite = this.appService.syssite
public licenceState = this.licenceService.licenceState public licenceState = this.licenceService.licenceState
public embed = globals.embed
constructor( constructor(
private appService: AppService, private appService: AppService,
@@ -143,6 +144,16 @@ export class AppComponent {
} }
}) })
const hashQuery = window.location.hash.split('?')[1]
if (hashQuery) {
const embedParam = new URLSearchParams(hashQuery).get('embed')
if (embedParam !== null) {
const isEmbed = embedParam !== 'false'
globals.embed = isEmbed
this.embed = isEmbed
}
}
this.subscribeToShowAbortModal() this.subscribeToShowAbortModal()
this.subscribeToRequestsModal() this.subscribeToRequestsModal()
this.subscribeToStartupData() this.subscribeToStartupData()

View File

@@ -165,7 +165,7 @@
class="card-header clr-row buttonBar headerBar clr-flex-md-row clr-justify-content-center clr-justify-content-lg-end" class="card-header clr-row buttonBar headerBar clr-flex-md-row clr-justify-content-center clr-justify-content-lg-end"
> >
<div <div
*ngIf="tableTrue" *ngIf="tableTrue && !embed"
class="clr-col-12 clr-col-md-3 clr-col-lg-4 backBtn" class="clr-col-12 clr-col-md-3 clr-col-lg-4 backBtn"
> >
<span <span

View File

@@ -13,6 +13,7 @@ import {
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import Handsontable from 'handsontable' import Handsontable from 'handsontable'
import { Subject, Subscription } from 'rxjs' import { Subject, Subscription } from 'rxjs'
import { sanitiseForSas } from '../shared/utils/sanitise'
import { SasStoreService } from '../services/sas-store.service' import { SasStoreService } from '../services/sas-store.service'
type AOA = any[][] type AOA = any[][]
@@ -264,6 +265,9 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
public badEdit = false public badEdit = false
public badEditCause: string | undefined public badEditCause: string | undefined
public badEditTitle: string | undefined public badEditTitle: string | undefined
get embed() {
return globals.embed
}
public tableTrue: boolean | undefined public tableTrue: boolean | undefined
public saveLoading = false public saveLoading = false
public approvers: string[] = [] public approvers: string[] = []
@@ -1666,7 +1670,7 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
this.submit = true this.submit = true
const updateParams: any = {} const updateParams: any = {}
updateParams.ACTION = 'LOAD' updateParams.ACTION = 'LOAD'
this.message = this.message.replace(/\n/g, '. ') this.message = sanitiseForSas(this.message.replace(/\n/g, '. '))
updateParams.MESSAGE = this.message updateParams.MESSAGE = this.message
// updateParams.APPROVER = this.approver; // updateParams.APPROVER = this.approver;
updateParams.LIBDS = this.libds updateParams.LIBDS = this.libds

View File

@@ -30,7 +30,7 @@ export const freeTierConfig: LicenceState = {
lineage_daily_limit: 3, lineage_daily_limit: 3,
tables_in_library_limit: 35, tables_in_library_limit: 35,
viewbox: true, viewbox: true,
fileUpload: true, fileUpload: false,
editRecord: true, editRecord: true,
addRecord: true addRecord: true
} }

View File

@@ -1,4 +1,5 @@
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { sanitiseForSas } from '../../shared/utils/sanitise'
import { SasStoreService } from '../../services/sas-store.service' import { SasStoreService } from '../../services/sas-store.service'
import { import {
Component, Component,
@@ -136,7 +137,7 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
public async rejecting() { public async rejecting() {
this.rejectLoading = true this.rejectLoading = true
this.submitReason = this.submitReason.replace(/\n/g, '. ') this.submitReason = sanitiseForSas(this.submitReason.replace(/\n/g, '. '))
let rejParams = { let rejParams = {
STP_ACTION: 'REJECT_TABLE', STP_ACTION: 'REJECT_TABLE',

View File

@@ -375,38 +375,30 @@ export class SpreadsheetUtil {
fileType: string fileType: string
): Promise<ParseResult> { ): Promise<ParseResult> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (this.licenceState.value.submit_rows_limit !== Infinity) { if (!this.licenceState.value.fileUpload) {
uploader.queue.pop() uploader.queue.pop()
return reject( return reject(
'Excel files only. To unlock CSV uploads, please contact support@datacontroller.io' 'File uploads are not enabled for this licence. Please contact support@datacontroller.io'
) )
} }
if (parseParams.encoding === 'WLATIN1') { if (parseParams.encoding !== 'WLATIN1') return resolve({ uploader })
let reader = new FileReader()
const self = this
// Closure to capture the file information.
reader.onload = (theFile: any) => {
let encoded = iconv.decode(
Buffer.from(theFile.target.result),
'CP-1252'
)
let blob = new Blob([encoded], { type: fileType })
let encodedFile: File = blobToFile(blob, parseParams.file.name)
uploader.queue.pop()
uploader.addToQueue([encodedFile])
return resolve({ const reader = new FileReader()
uploader reader.onload = (theFile) => {
}) if (!theFile.target?.result) return resolve({ uploader })
}
reader.readAsArrayBuffer(parseParams.file) const text = theFile.target.result as string
} else { const encoded = iconv.encode(text, 'CP-1252')
return resolve({ const blob = new Blob([encoded], { type: fileType })
uploader const encodedFile: File = blobToFile(blob, parseParams.file.name)
}) uploader.queue.pop()
uploader.addToQueue([encodedFile])
return resolve({ uploader })
} }
reader.readAsText(parseParams.file)
}) })
} }

View File

@@ -0,0 +1,6 @@
/**
* Strips characters that could cause SAS macro injection (& % ;).
*/
export function sanitiseForSas(input: string): string {
return input.replace(/[%&;]/g, '')
}

View File

@@ -236,7 +236,36 @@
<div class="admin-action"> <div class="admin-action">
Download Configuration Download Configuration
<button (click)="downloadConfiguration()" class="btn btn-info btn-sm"> <div class="libref-group">
<clr-tooltip class="libref-tooltip">
<label clrTooltipTrigger class="libref-label">
Target DC Library
<cds-icon shape="info-circle" size="16"></cds-icon>
</label>
<clr-tooltip-content
clrPosition="bottom-left"
clrSize="md"
*clrIfOpen
>
Enter the target DC library and the downloaded files will
contain this, instead of the original.
</clr-tooltip-content>
</clr-tooltip>
<input
type="text"
class="clr-input libref-input"
maxlength="8"
[ngModel]="dcLib"
(ngModelChange)="targetLibref = $event.toUpperCase()"
placeholder="e.g. MYLIB"
/>
</div>
<button
(click)="downloadConfiguration()"
[disabled]="targetLibref !== dcLib && !isValidLibref(targetLibref)"
class="btn btn-info btn-sm"
>
DOWNLOAD DOWNLOAD
</button> </button>
</div> </div>

View File

@@ -0,0 +1,21 @@
.libref-group {
display: inline-flex;
align-items: center;
gap: 4px;
margin: 0 8px;
}
.libref-label {
cursor: pointer;
font-size: 0.55rem;
font-weight: 600;
color: var(--clr-p4-color, #565656);
display: inline-flex;
align-items: center;
gap: 4px;
}
.libref-input {
width: 100px;
text-transform: uppercase;
}

View File

@@ -10,6 +10,7 @@ import { EnvironmentInfo } from './models/environment-info.model'
import { AppSettingsService } from '../services/app-settings.service' import { AppSettingsService } from '../services/app-settings.service'
import { AppSettings } from '../models/AppSettings' import { AppSettings } from '../models/AppSettings'
import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapperResponse' import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapperResponse'
import { globals } from '../_globals'
@Component({ @Component({
selector: 'app-system', selector: 'app-system',
@@ -39,6 +40,8 @@ export class SystemComponent implements OnInit {
responseModal: boolean = false responseModal: boolean = false
Infinity = Infinity Infinity = Infinity
dcLib: string = globals.dcLib
targetLibref: string = globals.dcLib
licenceState = this.licenceService.licenceState licenceState = this.licenceService.licenceState
settings: AppSettings settings: AppSettings
@@ -71,13 +74,21 @@ export class SystemComponent implements OnInit {
this.appSettingsService.setAppSettings(this.settings) this.appSettingsService.setAppSettings(this.settings)
} }
isValidLibref(value: string): boolean {
return /^[A-Za-z_]\w{0,7}$/.test(value.trim())
}
downloadConfiguration() { downloadConfiguration() {
let sasjsConfig = this.sasService.getSasjsConfig() let sasjsConfig = this.sasService.getSasjsConfig()
let storage = sasjsConfig.serverUrl let storage = sasjsConfig.serverUrl
let metaData = sasjsConfig.appLoc let metaData = sasjsConfig.appLoc
let path = this.sasService.getExecutionPath() let path = this.sasService.getExecutionPath()
let lib = this.targetLibref.toUpperCase().trim()
let downUrl = let downUrl =
storage + path + '/?_program=' + metaData + '/services/admin/exportconfig' storage + path + '/?_program=' + metaData + '/services/admin/exportconfig'
if (lib && lib !== this.dcLib && this.isValidLibref(lib)) {
downUrl += '&dclib=' + encodeURIComponent(lib)
}
window.open(downUrl) window.open(downUrl)
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "dcfrontend", "name": "dcfrontend",
"version": "7.4.0", "version": "7.6.0",
"description": "Data Controller", "description": "Data Controller",
"devDependencies": { "devDependencies": {
"@saithodev/semantic-release-gitea": "^2.1.0", "@saithodev/semantic-release-gitea": "^2.1.0",

2
sas/.npmrc Normal file
View File

@@ -0,0 +1,2 @@
ignore-scripts=true
save-exact=true

439
sas/package-lock.json generated
View File

@@ -6,8 +6,32 @@
"": { "": {
"name": "dc-sas", "name": "dc-sas",
"dependencies": { "dependencies": {
"@sasjs/cli": "^4.14.0", "@sasjs/cli": "4.15.2",
"@sasjs/core": "^4.61.0" "@sasjs/core": "4.63.0"
}
},
"node_modules/@asamuzakjp/css-color": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
"license": "MIT",
"dependencies": {
"@csstools/css-calc": "^2.1.3",
"@csstools/css-color-parser": "^3.0.9",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"lru-cache": "^10.4.3"
}
},
"node_modules/@asamuzakjp/dom-selector": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-2.0.2.tgz",
"integrity": "sha512-x1KXOatwofR6ZAYzXRBL5wrdV0vwNxlTCK9NCuLqAzQYARqGcvFwiJA6A1ERuh+dgeA4Dxm3JBYictIes+SqUQ==",
"license": "MIT",
"dependencies": {
"bidi-js": "^1.0.3",
"css-tree": "^2.3.1",
"is-potential-custom-element-name": "^1.0.1"
} }
}, },
"node_modules/@coolaj86/urequest": { "node_modules/@coolaj86/urequest": {
@@ -16,6 +40,118 @@
"integrity": "sha512-PPrVYra9aWvZjSCKl/x1pJ9ZpXda1652oJrPBYy5rQumJJMkmTBN3ux+sK2xAUwVvv2wnewDlaQaHLxLwSHnIA==", "integrity": "sha512-PPrVYra9aWvZjSCKl/x1pJ9ZpXda1652oJrPBYy5rQumJJMkmTBN3ux+sK2xAUwVvv2wnewDlaQaHLxLwSHnIA==",
"license": "(MIT OR Apache-2.0)" "license": "(MIT OR Apache-2.0)"
}, },
"node_modules/@csstools/color-helpers": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT-0",
"engines": {
"node": ">=18"
}
},
"node_modules/@csstools/css-calc": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.5",
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-color-parser": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"dependencies": {
"@csstools/color-helpers": "^5.1.0",
"@csstools/css-calc": "^2.1.4"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.5",
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-parser-algorithms": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-tokenizer": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@fast-csv/format": { "node_modules/@fast-csv/format": {
"version": "4.3.5", "version": "4.3.5",
"resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz",
@@ -115,13 +251,13 @@
} }
}, },
"node_modules/@sasjs/cli": { "node_modules/@sasjs/cli": {
"version": "4.14.0", "version": "4.15.2",
"resolved": "https://registry.npmjs.org/@sasjs/cli/-/cli-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@sasjs/cli/-/cli-4.15.2.tgz",
"integrity": "sha512-WpZFLxPuh0xBPfX4Vy5kkhvz2QVRLmOwmw70rdHd5DpSw3U4CGY3EbCLVSd0K0CLEBWJLR5EX2gITV8hUcCP4w==", "integrity": "sha512-lY9H+HIquLAPXuhk6ov/xyBooERvefT6oiwNRaQ6DHMMFE4cgPvrUH5s3RRkLI2+lET0M0hPPbuaZ4w9yFIDuA==",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@sasjs/adapter": "4.16.3", "@sasjs/adapter": "4.16.3",
"@sasjs/core": "4.61.0", "@sasjs/core": "4.63.0",
"@sasjs/lint": "2.4.3", "@sasjs/lint": "2.4.3",
"@sasjs/utils": "3.5.6", "@sasjs/utils": "3.5.6",
"adm-zip": "0.5.10", "adm-zip": "0.5.10",
@@ -129,7 +265,7 @@
"dotenv": "16.0.3", "dotenv": "16.0.3",
"find": "0.3.0", "find": "0.3.0",
"js-base64": "3.7.5", "js-base64": "3.7.5",
"jsdom": "22.1.0", "jsdom": "23.2.0",
"jwt-decode": "3.1.2", "jwt-decode": "3.1.2",
"lodash.groupby": "4.6.0", "lodash.groupby": "4.6.0",
"lodash.uniqby": "4.7.0", "lodash.uniqby": "4.7.0",
@@ -181,9 +317,9 @@
} }
}, },
"node_modules/@sasjs/core": { "node_modules/@sasjs/core": {
"version": "4.61.0", "version": "4.63.0",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.61.0.tgz", "resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.63.0.tgz",
"integrity": "sha512-gpewnAUBDqPOSR1PsRPCB6vba+kY5NL6UyYuSUZFVh1j9Mz5Wkli3eZZjStZABflqKQVQbPNsgIlqw/SpzwGxg==", "integrity": "sha512-NlIihA4BbP+mveAbb7A/hgnrZEpJKKIkq0v4SSDdYXg8YYdKAdyTK8K+6FNPwp+U6hixQCKVX8oCA1DIUppLqA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@sasjs/lint": { "node_modules/@sasjs/lint": {
@@ -233,15 +369,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/@tootallnate/once": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
"integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
"license": "MIT",
"engines": {
"node": ">= 10"
}
},
"node_modules/@types/fs-extra": { "node_modules/@types/fs-extra": {
"version": "11.0.4", "version": "11.0.4",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz",
@@ -276,13 +403,6 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
"integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
"deprecated": "Use your platform's native atob() and btoa() methods instead",
"license": "BSD-3-Clause"
},
"node_modules/accumulate-stream": { "node_modules/accumulate-stream": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/accumulate-stream/-/accumulate-stream-5.0.0.tgz", "resolved": "https://registry.npmjs.org/accumulate-stream/-/accumulate-stream-5.0.0.tgz",
@@ -407,6 +527,15 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/bidi-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
"license": "MIT",
"dependencies": {
"require-from-string": "^2.0.2"
}
},
"node_modules/bl": { "node_modules/bl": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@@ -624,30 +753,49 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/cssstyle": { "node_modules/css-tree": {
"version": "3.0.0", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
"integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"rrweb-cssom": "^0.6.0" "mdn-data": "2.0.30",
"source-map-js": "^1.0.1"
}, },
"engines": { "engines": {
"node": ">=14" "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
} }
}, },
"node_modules/data-urls": { "node_modules/cssstyle": {
"version": "4.0.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
"integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"abab": "^2.0.6", "@asamuzakjp/css-color": "^3.2.0",
"whatwg-mimetype": "^3.0.0", "rrweb-cssom": "^0.8.0"
"whatwg-url": "^12.0.0"
}, },
"engines": { "engines": {
"node": ">=14" "node": ">=18"
}
},
"node_modules/cssstyle/node_modules/rrweb-cssom": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
"license": "MIT"
},
"node_modules/data-urls": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
"license": "MIT",
"dependencies": {
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.0.0"
},
"engines": {
"node": ">=18"
} }
}, },
"node_modules/debug": { "node_modules/debug": {
@@ -694,19 +842,6 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/domexception": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
"integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==",
"deprecated": "Use your platform's native DOMException instead",
"license": "MIT",
"dependencies": {
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.0.3", "version": "16.0.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
@@ -1073,15 +1208,15 @@
} }
}, },
"node_modules/html-encoding-sniffer": { "node_modules/html-encoding-sniffer": {
"version": "3.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
"integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"whatwg-encoding": "^2.0.0" "whatwg-encoding": "^3.1.1"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/http-cookie-agent": { "node_modules/http-cookie-agent": {
@@ -1109,29 +1244,16 @@
} }
}, },
"node_modules/http-proxy-agent": { "node_modules/http-proxy-agent": {
"version": "5.0.0", "version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tootallnate/once": "2", "agent-base": "^7.1.0",
"agent-base": "6", "debug": "^4.3.4"
"debug": "4"
}, },
"engines": { "engines": {
"node": ">= 6" "node": ">= 14"
}
},
"node_modules/http-proxy-agent/node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
} }
}, },
"node_modules/https": { "node_modules/https": {
@@ -1141,28 +1263,16 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/https-proxy-agent": { "node_modules/https-proxy-agent": {
"version": "5.0.1", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"agent-base": "6", "agent-base": "^7.1.2",
"debug": "4" "debug": "4"
}, },
"engines": { "engines": {
"node": ">= 6" "node": ">= 14"
}
},
"node_modules/https-proxy-agent/node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
} }
}, },
"node_modules/human-signals": { "node_modules/human-signals": {
@@ -1339,40 +1449,38 @@
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/jsdom": { "node_modules/jsdom": {
"version": "22.1.0", "version": "23.2.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-23.2.0.tgz",
"integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", "integrity": "sha512-L88oL7D/8ufIES+Zjz7v0aes+oBMh2Xnh3ygWvL0OaICOomKEPKuPnIfBJekiXr+BHbbMjrWn/xqrDQuxFTeyA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"abab": "^2.0.6", "@asamuzakjp/dom-selector": "^2.0.1",
"cssstyle": "^3.0.0", "cssstyle": "^4.0.1",
"data-urls": "^4.0.0", "data-urls": "^5.0.0",
"decimal.js": "^10.4.3", "decimal.js": "^10.4.3",
"domexception": "^4.0.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"html-encoding-sniffer": "^3.0.0", "html-encoding-sniffer": "^4.0.0",
"http-proxy-agent": "^5.0.0", "http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^5.0.1", "https-proxy-agent": "^7.0.2",
"is-potential-custom-element-name": "^1.0.1", "is-potential-custom-element-name": "^1.0.1",
"nwsapi": "^2.2.4",
"parse5": "^7.1.2", "parse5": "^7.1.2",
"rrweb-cssom": "^0.6.0", "rrweb-cssom": "^0.6.0",
"saxes": "^6.0.0", "saxes": "^6.0.0",
"symbol-tree": "^3.2.4", "symbol-tree": "^3.2.4",
"tough-cookie": "^4.1.2", "tough-cookie": "^4.1.3",
"w3c-xmlserializer": "^4.0.0", "w3c-xmlserializer": "^5.0.0",
"webidl-conversions": "^7.0.0", "webidl-conversions": "^7.0.0",
"whatwg-encoding": "^2.0.0", "whatwg-encoding": "^3.1.1",
"whatwg-mimetype": "^3.0.0", "whatwg-mimetype": "^4.0.0",
"whatwg-url": "^12.0.1", "whatwg-url": "^14.0.0",
"ws": "^8.13.0", "ws": "^8.16.0",
"xml-name-validator": "^4.0.0" "xml-name-validator": "^5.0.0"
}, },
"engines": { "engines": {
"node": ">=16" "node": ">=18"
}, },
"peerDependencies": { "peerDependencies": {
"canvas": "^2.5.0" "canvas": "^2.11.2"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"canvas": { "canvas": {
@@ -1475,6 +1583,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC"
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -1484,6 +1598,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/mdn-data": {
"version": "2.0.30",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
"license": "CC0-1.0"
},
"node_modules/merge-stream": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -1594,12 +1714,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/nwsapi": {
"version": "2.2.23",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
"integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
"license": "MIT"
},
"node_modules/onetime": { "node_modules/onetime": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
@@ -1706,9 +1820,9 @@
} }
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8.6" "node": ">=8.6"
@@ -1806,6 +1920,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/requires-port": { "node_modules/requires-port": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -1948,6 +2071,15 @@
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/ssl-root-cas": { "node_modules/ssl-root-cas": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/ssl-root-cas/-/ssl-root-cas-1.3.1.tgz", "resolved": "https://registry.npmjs.org/ssl-root-cas/-/ssl-root-cas-1.3.1.tgz",
@@ -2057,15 +2189,15 @@
} }
}, },
"node_modules/tr46": { "node_modules/tr46": {
"version": "4.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
"integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"punycode": "^2.3.0" "punycode": "^2.3.1"
}, },
"engines": { "engines": {
"node": ">=14" "node": ">=18"
} }
}, },
"node_modules/traverse-chain": { "node_modules/traverse-chain": {
@@ -2111,15 +2243,15 @@
"integrity": "sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==" "integrity": "sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA=="
}, },
"node_modules/w3c-xmlserializer": { "node_modules/w3c-xmlserializer": {
"version": "4.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
"integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"xml-name-validator": "^4.0.0" "xml-name-validator": "^5.0.0"
}, },
"engines": { "engines": {
"node": ">=14" "node": ">=18"
} }
}, },
"node_modules/wcwidth": { "node_modules/wcwidth": {
@@ -2141,37 +2273,38 @@
} }
}, },
"node_modules/whatwg-encoding": { "node_modules/whatwg-encoding": {
"version": "2.0.0", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
"integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"iconv-lite": "0.6.3" "iconv-lite": "0.6.3"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/whatwg-mimetype": { "node_modules/whatwg-mimetype": {
"version": "3.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/whatwg-url": { "node_modules/whatwg-url": {
"version": "12.0.1", "version": "14.2.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
"integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tr46": "^4.1.1", "tr46": "^5.1.0",
"webidl-conversions": "^7.0.0" "webidl-conversions": "^7.0.0"
}, },
"engines": { "engines": {
"node": ">=14" "node": ">=18"
} }
}, },
"node_modules/which": { "node_modules/which": {
@@ -2207,9 +2340,9 @@
} }
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.18.3", "version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
@@ -2234,12 +2367,12 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/xml-name-validator": { "node_modules/xml-name-validator": {
"version": "4.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
"integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/xmlchars": { "node_modules/xmlchars": {

View File

@@ -28,7 +28,7 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@sasjs/cli": "^4.14.0", "@sasjs/cli": "4.15.2",
"@sasjs/core": "^4.61.0" "@sasjs/core": "4.63.0"
} }
} }

View File

@@ -0,0 +1,52 @@
/**
@file
@brief migration script to move from v7.0 to v7.6 of data controller
OPTIONAL CHANGE - upload additional data as placeholders for modifying the
default email message
**/
%let dclib=YOURDCLIB;
libname &dclib "/YOUR/DATACONTROLLER/LIBRARY/PATH";
proc sql;
insert into &dclib..mpe_config set
tx_from=%sysfunc(datetime())
,tx_to='31DEC9999:23:59:59'dt
,var_scope="DC_EMAIL"
,var_name="SUBMITTED_TEMPLATE"
,var_value='Dear user,'!!'0A'x!!'Please be advised that a change to table'
!!' &alert_lib..&alert_ds has been proposed by &from_user on the '
!!'&syshostname SAS server.'!!'0A'x!!'Reason provided: '
!!'%superq(SUBMITTED_TXT)'
!!'0A'x!!'This is an automated email by Data Controller for SAS. For '
!!'documentation, please visit https://docs.datacontroller.io'
,var_active=1
,var_desc='Template email, sent after submitting a change';
insert into &dclib..mpe_config set
tx_from=%sysfunc(datetime())
,tx_to='31DEC9999:23:59:59'dt
,var_scope="DC_EMAIL"
,var_name="APPROVED_TEMPLATE"
,var_value='Dear user,'!!'0A'x!!'Please be advised that a change to table'
!!' &alert_lib..&alert_ds has been approved by &from_user on the '
!!'&syshostname SAS server.'!!'0A'x!!'This is an automated email by Data'
!!' Controller for SAS. For documentation, please visit '
!!'https://docs.datacontroller.io'
,var_active=1
,var_desc='Template email, sent after approving a change';
insert into &dclib..mpe_config set
tx_from=%sysfunc(datetime())
,tx_to='31DEC9999:23:59:59'dt
,var_scope="DC_EMAIL"
,var_name="REJECTED_TEMPLATE"
,var_value='Dear user,'!!'0A'x!!'Please be advised that a change to table'
!!' &alert_lib..&alert_ds has been rejected by &from_user on the '
!!'&syshostname SAS server.'!!'0A'x!!'Reason provided: '
!!'%superq(REVIEW_REASON_TXT)'
!!'0A'x!!'This is an automated email by Data Controller for SAS. For '
!!'documentation, please visit https://docs.datacontroller.io'
,var_active=1
,var_desc='Template email, sent after rejecting a change';

View File

@@ -26,8 +26,7 @@ NOTES:
One cannot use BETWEEN One cannot use BETWEEN
One cannot use &xx_from LE [tstamp] LE &xx_from (equivalent to above). One cannot use &xx_from LE [tstamp] LE &xx_from (equivalent to above).
Background: Background: https://stackoverflow.com/questions/20005950
http://stackoverflow.com/questions/20005950/best-practice-for-scd-date-pairs-closing-opening-timestamps
Areas for optimisation Areas for optimisation
- loading temporal history (currently experimental) - loading temporal history (currently experimental)
@@ -220,8 +219,8 @@ Areas for optimisation
%local engine_type; %local engine_type;
%let engine_type=%mf_getengine(&base_lib); %let engine_type=%mf_getengine(&base_lib);
%if (&engine_type=REDSHIFT or &engine_type=POSTGRES or &engine_type=SNOW) %if %length(&CLOSE_VARS)>0 and (&engine_type=REDSHIFT or &engine_type=POSTGRES
and %length(&CLOSE_VARS)>0 or &engine_type=SNOW or &engine_type=SASIOSNF)
%then %do; %then %do;
%put NOTE:; %put NOTE-;%put NOTE-;%put NOTE-; %put NOTE:; %put NOTE-;%put NOTE-;%put NOTE-;
%put NOTE- CLOSE_VARS functionality not yet supported in &engine_type; %put NOTE- CLOSE_VARS functionality not yet supported in &engine_type;
@@ -638,6 +637,7 @@ data work.bitemp0_append &keepvars &outds_del(drop=&md5_col )
create table work.bitemp0_base as select * from connection to myAlias( create table work.bitemp0_base as select * from connection to myAlias(
%end; %end;
%else %if &engine_type=REDSHIFT or &engine_type=POSTGRES or &engine_type=SNOW %else %if &engine_type=REDSHIFT or &engine_type=POSTGRES or &engine_type=SNOW
or &engine_type=SASIOSNF
%then %do; %then %do;
/* grab schema */ /* grab schema */
%let baselib_schema=%mf_getschema(&base_lib); %let baselib_schema=%mf_getschema(&base_lib);
@@ -661,7 +661,7 @@ data work.bitemp0_append &keepvars &outds_del(drop=&md5_col )
%else %let base_table=&baselib_schema.&base_dsn; %else %let base_table=&baselib_schema.&base_dsn;
/* make in-db empty table with PK + MD5 only */ /* make in-db empty table with PK + MD5 only */
%dc_assignlib(WRITE,&base_lib,passthru=myAlias) %dc_assignlib(WRITE,&base_lib,passthru=myAlias)
%if &engine_type=SNOW %then %do; %if &engine_type=SNOW or &engine_type=SASIOSNF %then %do;
exec (create transient table &baselib_schema.&temp_table exec (create transient table &baselib_schema.&temp_table
like &baselib_schema.&base_dsn like &baselib_schema.&base_dsn
) by myAlias; ) by myAlias;
@@ -686,9 +686,12 @@ data work.bitemp0_append &keepvars &outds_del(drop=&md5_col )
exec (alter table &temp_table add column &md5_col varchar(32);) by myAlias; exec (alter table &temp_table add column &md5_col varchar(32);) by myAlias;
/* create view to strip formats and avoid warns in log */ /* create view to strip formats and avoid warns in log */
data work.vw_bitemp0/view=work.vw_bitemp0; data work.vw_bitemp0/view=work.vw_bitemp0;
/* inherit remote length to handle byte expansion */
if 0 then set &base_lib..&temp_table(keep=&md5_col);
set work.bitemp0_append(keep=&pk &md5_col); set work.bitemp0_append(keep=&pk &md5_col);
format _all_; format _all_;
run; run;
proc append base=&base_lib..&temp_table proc append base=&base_lib..&temp_table
%if &engine_type=REDSHIFT %then %do; %if &engine_type=REDSHIFT %then %do;
( (
@@ -741,7 +744,7 @@ data work.bitemp0_append &keepvars &outds_del(drop=&md5_col )
%if &engine_type=OLEDB or &engine_type=REDSHIFT or &engine_type=POSTGRES %if &engine_type=OLEDB or &engine_type=REDSHIFT or &engine_type=POSTGRES
or &engine_type=SNOW or &engine_type=SNOW or &engine_type=SASIOSNF
%then %do; %then %do;
); proc sql; drop table &base_lib.."&temp_table"n; ); proc sql; drop table &base_lib.."&temp_table"n;
%end; %end;
@@ -1196,7 +1199,7 @@ run;
%else %if (&loadtype=BITEMPORAL or &loadtype=TXTEMPORAL or &loadtype=UPDATE) %else %if (&loadtype=BITEMPORAL or &loadtype=TXTEMPORAL or &loadtype=UPDATE)
%then %do; %then %do;
data _null_; data _null_;
putlog "&sysmacroname: &loadtype operation using &engine_type engine"; putlog "&sysmacroname: &loadtype operation using *&engine_type* engine";
run; run;
%local flexinow; %local flexinow;
proc sql; proc sql;
@@ -1213,6 +1216,7 @@ run;
execute( execute(
%end; %end;
%else %if &engine_type=REDSHIFT or &engine_type=POSTGRES or &engine_type=SNOW %else %if &engine_type=REDSHIFT or &engine_type=POSTGRES or &engine_type=SNOW
or &engine_type=SASIOSNF
%then %do; %then %do;
%let innertable=%upcase(%mf_getuniquename(prefix=XDCTEMP)); %let innertable=%upcase(%mf_getuniquename(prefix=XDCTEMP));
%let top_table=&baselib_schema.&base_dsn; %let top_table=&baselib_schema.&base_dsn;
@@ -1220,7 +1224,7 @@ run;
/* make empty table first - must clone & drop extra cols /* make empty table first - must clone & drop extra cols
as autoload is bad */ as autoload is bad */
%dc_assignlib(WRITE,&base_lib,passthru=myAlias) %dc_assignlib(WRITE,&base_lib,passthru=myAlias)
%if &engine_type=SNOW %then %do; %if &engine_type=SNOW or &engine_type=SASIOSNF %then %do;
exec (create transient table &baselib_schema.&innertable exec (create transient table &baselib_schema.&innertable
like &baselib_schema.&base_dsn like &baselib_schema.&base_dsn
) by myAlias; ) by myAlias;
@@ -1259,6 +1263,7 @@ run;
execute( execute(
%end; %end;
%else %do; %else %do;
%put Not using passthrough for *&engine_type* engine;
%let innertable=bitemp5d_subquery; %let innertable=bitemp5d_subquery;
%let top_table=&base_lib..&base_dsn; %let top_table=&base_lib..&base_dsn;
%let flexinow=&now; %let flexinow=&now;
@@ -1311,7 +1316,7 @@ run;
1=1); 1=1);
%if &engine_type=OLEDB or &engine_type=REDSHIFT or &engine_type=POSTGRES %if &engine_type=OLEDB or &engine_type=REDSHIFT or &engine_type=POSTGRES
or &engine_type=SNOW or &engine_type=SNOW or &engine_type=SASIOSNF
%then %do; %then %do;
) by myAlias; ) by myAlias;
execute (drop table &baselib_schema.&innertable) by myAlias; execute (drop table &baselib_schema.&innertable) by myAlias;

View File

@@ -127,6 +127,11 @@ run;
filename __out email (&emails) filename __out email (&emails)
subject="Table &alert_lib..&alert_ds has been &alert_event"; subject="Table &alert_lib..&alert_ds has been &alert_event";
data work.alertmessage;
set &mpelib..mpe_config;
where &dc_dttmtfmt. lt tx_to;
where also var_scope='DC_EMAIL' and var_name="&alert_event._TEMPLATE";
run;
%local SUBMITTED_TXT; %local SUBMITTED_TXT;
%if &alert_event=SUBMITTED %then %do; %if &alert_event=SUBMITTED %then %do;
data _null_; data _null_;
@@ -136,30 +141,54 @@ filename __out email (&emails)
run; run;
data _null_; data _null_;
File __out lrecl=32000; File __out lrecl=32000;
length txt $2048;
%if %mf_getattrn(alertmessage,NLOBS)=0 %then %do;
put 'Dear user,'; put 'Dear user,';
put ' '; put ' ';
put "Please be advised that a change to table &alert_lib..&alert_ds has " put "Please be advised that a change to table &alert_lib..&alert_ds has "
"been proposed by &from_user on the '&syshostname' SAS server."; "been proposed by &from_user on the &syshostname SAS server.";
put " "; put " ";
length txt $2048;
txt=symget('SUBMITTED_TXT'); txt=symget('SUBMITTED_TXT');
put "Reason provided: " txt; put "Reason provided: " txt;
put " "; put " ";
put "This is an automated email by Data Controller for SAS. For " put "This is an automated email by Data Controller for SAS. For "
"documentation, please visit https://docs.datacontroller.io"; "documentation, please visit https://docs.datacontroller.io";
%end;
%else %do;
/* take template from config table */
set work.alertmessage;
cnt=countw(var_value,'0A'x);
do i=1 to cnt;
txt=resolve(scan(var_value,i,'0A'x));
put txt /;
end;
%end;
run; run;
%end; %end;
%else %if &alert_event=APPROVED %then %do; %else %if &alert_event=APPROVED %then %do;
/* there is no approval message */ /* there is no approval message */
data _null_; data _null_;
File __out lrecl=32000; File __out lrecl=32000;
length txt $2048;
%if %mf_getattrn(alertmessage,NLOBS)=0 %then %do;
/* fallback message */
put 'Dear user,'; put 'Dear user,';
put ' '; put ' ';
put "Please be advised that a change to table &alert_lib..&alert_ds has " put "Please be advised that a change to table &alert_lib..&alert_ds has "
"been approved by &from_user on the '&syshostname' SAS server."; "been approved by &from_user on the &syshostname SAS server.";
put " "; put " ";
put "This is an automated email by Data Controller for SAS. For " put "This is an automated email by Data Controller for SAS. For "
"documentation, please visit https://docs.datacontroller.io"; "documentation, please visit https://docs.datacontroller.io";
%end;
%else %do;
/* take template from config table */
set work.alertmessage;
cnt=countw(var_value,'0A'x);
do i=1 to cnt;
txt=resolve(scan(var_value,i,'0A'x));
put txt /;
end;
%end;
run; run;
%end; %end;
%else %if &alert_event=REJECTED %then %do; %else %if &alert_event=REJECTED %then %do;
@@ -170,17 +199,29 @@ filename __out email (&emails)
run; run;
data _null_; data _null_;
File __out lrecl=32000; File __out lrecl=32000;
length txt $2048;
%if %mf_getattrn(alertmessage,NLOBS)=0 %then %do;
/* fallback message */
put 'Dear user,'; put 'Dear user,';
put ' '; put ' ';
put "Please be advised that a change to table &alert_lib..&alert_ds has " put "Please be advised that a change to table &alert_lib..&alert_ds has "
"been rejected by &from_user on the '&syshostname' SAS server."; "been rejected by &from_user on the &syshostname SAS server.";
put " "; put " ";
length txt $2048;
txt=symget('REVIEW_REASON_TXT'); txt=symget('REVIEW_REASON_TXT');
put "Reason provided: " txt; put "Reason provided: " txt;
put " "; put " ";
put "This is an automated email by Data Controller for SAS. For " put "This is an automated email by Data Controller for SAS. For "
"documentation, please visit https://docs.datacontroller.io"; "documentation, please visit https://docs.datacontroller.io";
%end;
%else %do;
/* take template from config table */
set work.alertmessage;
cnt=countw(var_value,'0A'x);
do i=1 to cnt;
txt=resolve(scan(var_value,i,'0A'x));
put txt /;
end;
%end;
run; run;
%end; %end;

View File

@@ -201,6 +201,44 @@ insert into &lib..mpe_config set
,var_value=' ' ,var_value=' '
,var_active=1 ,var_active=1
,var_desc='Activation Key'; ,var_desc='Activation Key';
insert into &lib..mpe_config set
tx_from=0
,tx_to='31DEC9999:23:59:59'dt
,var_scope="DC_EMAIL"
,var_name="SUBMITTED_TEMPLATE"
,var_value='Dear user,'!!'0A'x!!'Please be advised that a change to table'
!!' &alert_lib..&alert_ds has been proposed by &from_user on the '
!!'&syshostname SAS server.'!!'0A'x!!'Reason provided: '
!!'%superq(SUBMITTED_TXT)'
!!'0A'x!!'This is an automated email by Data Controller for SAS. For '
!!'documentation, please visit https://docs.datacontroller.io'
,var_active=1
,var_desc='Template email, sent after submitting a change';
insert into &lib..mpe_config set
tx_from=0
,tx_to='31DEC9999:23:59:59'dt
,var_scope="DC_EMAIL"
,var_name="APPROVED_TEMPLATE"
,var_value='Dear user,'!!'0A'x!!'Please be advised that a change to table'
!!' &alert_lib..&alert_ds has been approved by &from_user on the '
!!'&syshostname SAS server.'!!'0A'x!!'This is an automated email by Data'
!!' Controller for SAS. For documentation, please visit '
!!'https://docs.datacontroller.io'
,var_active=1
,var_desc='Template email, sent after approving a change';
insert into &lib..mpe_config set
tx_from=0
,tx_to='31DEC9999:23:59:59'dt
,var_scope="DC_EMAIL"
,var_name="REJECTED_TEMPLATE"
,var_value='Dear user,'!!'0A'x!!'Please be advised that a change to table'
!!' &alert_lib..&alert_ds has been rejected by &from_user on the '
!!'&syshostname SAS server.'!!'0A'x!!'Reason provided: '
!!'%superq(REVIEW_REASON_TXT)'
!!'0A'x!!'This is an automated email by Data Controller for SAS. For '
!!'documentation, please visit https://docs.datacontroller.io'
,var_active=1
,var_desc='Template email, sent after rejecting a change';
insert into &lib..mpe_datadictionary set insert into &lib..mpe_datadictionary set
@@ -213,7 +251,6 @@ insert into &lib..mpe_datadictionary set
,DD_RESPONSIBLE="&sysuserid" ,DD_RESPONSIBLE="&sysuserid"
,DD_SENSITIVITY="Low" ,DD_SENSITIVITY="Low"
,tx_to='31DEC5999:23:59:59'dt; ,tx_to='31DEC5999:23:59:59'dt;
insert into &lib..mpe_datadictionary set insert into &lib..mpe_datadictionary set
tx_from=0 tx_from=0
,DD_TYPE='TABLE' ,DD_TYPE='TABLE'
@@ -224,7 +261,6 @@ insert into &lib..mpe_datadictionary set
,DD_RESPONSIBLE="&sysuserid" ,DD_RESPONSIBLE="&sysuserid"
,DD_SENSITIVITY="Low" ,DD_SENSITIVITY="Low"
,tx_to='31DEC5999:23:59:59'dt; ,tx_to='31DEC5999:23:59:59'dt;
insert into &lib..mpe_datadictionary set insert into &lib..mpe_datadictionary set
tx_from=0 tx_from=0
,DD_TYPE='COLUMN' ,DD_TYPE='COLUMN'
@@ -235,7 +271,6 @@ insert into &lib..mpe_datadictionary set
,DD_RESPONSIBLE="&sysuserid" ,DD_RESPONSIBLE="&sysuserid"
,DD_SENSITIVITY="Low" ,DD_SENSITIVITY="Low"
,tx_to='31DEC5999:23:59:59'dt; ,tx_to='31DEC5999:23:59:59'dt;
insert into &lib..mpe_datadictionary set insert into &lib..mpe_datadictionary set
tx_from=0 tx_from=0
,DD_TYPE='DIRECTORY' ,DD_TYPE='DIRECTORY'

View File

@@ -9,10 +9,12 @@
<h4> SAS Macros </h4> <h4> SAS Macros </h4>
@li mf_getuser.sas @li mf_getuser.sas
@li mf_nobs.sas @li mf_nobs.sas
@li mp_ds2cards.sas
@li mp_abort.sas @li mp_abort.sas
@li mp_binarycopy.sas @li mp_binarycopy.sas
@li mp_ds2cards.sas
@li mp_ds2csv.sas
@li mp_streamfile.sas @li mp_streamfile.sas
@li mp_validatecol.sas
@author 4GL Apps Ltd @author 4GL Apps Ltd
@copyright 4GL Apps Ltd. This code may only be used within Data Controller @copyright 4GL Apps Ltd. This code may only be used within Data Controller
@@ -21,23 +23,33 @@
**/ **/
%global dclib islib newlib;
%mpeinit() %mpeinit()
data _null_;
newlib=coalescec(symget('dclib'),"&mpelib");
%mp_validatecol(newlib,ISLIB,islib)
call symputx('islib',islib);
call symputx('newlib',upcase(newlib));
putlog (_all_)(=);
run;
%mp_abort(iftrue= (&islib ne 1)
,mac=&_program
,msg=%nrstr(&newlib is not a valid libref)
)
%let work=%sysfunc(pathname(work)); %let work=%sysfunc(pathname(work));
/* excel does not work in all envs */
%let mime=application/vnd.ms-excel;
%let dbms=EXCEL;
%let mime=application/csv; %let mime=application/csv;
%let dbms=CSV; %let dbms=CSV;
%let ext=csv; %let ext=csv;
%macro conditional_export(ds); %macro conditional_export(ds);
%if %mf_nobs(&ds)>0 %then %do; %if %mf_nobs(&ds)>0 %then %do;
PROC EXPORT DATA= &ds OUTFILE= "&work/&ds..&ext" /* cannot use PROC EXPORT as we need to wrap all csv char values in quotes */
DBMS=&dbms REPLACE; /* cannot use excel as it does not work consistently in all SAS envs */
RUN; %mp_ds2csv(&ds,outfile="&work/&newlib..&ds..csv",headerformat=NAME)
ods package(ProdOutput) add file="&work/&ds..&ext" mimetype="&mime"; ods package(ProdOutput) add file="&work/&newlib..&ds..&ext" mimetype="&mime";
%end; %end;
%mp_abort(iftrue= (&syscc ne 0) %mp_abort(iftrue= (&syscc ne 0)
,mac=&_program ,mac=&_program
@@ -52,6 +64,7 @@ data MPE_ALERTS;
set &mpelib..MPE_ALERTS; set &mpelib..MPE_ALERTS;
where &dc_dttmtfmt. le tx_to; where &dc_dttmtfmt. le tx_to;
drop tx_: ; drop tx_: ;
if alert_lib="&mpelib" then alert_lib="&newlib";
run; run;
%conditional_export(MPE_ALERTS) %conditional_export(MPE_ALERTS)
@@ -61,6 +74,7 @@ data MPE_COLUMN_LEVEL_SECURITY;
where &dc_dttmtfmt. le tx_to; where &dc_dttmtfmt. le tx_to;
where also CLS_LIBREF ne "&mpelib"; where also CLS_LIBREF ne "&mpelib";
drop tx_: ; drop tx_: ;
CLS_LIBREF="&newlib";
run; run;
%conditional_export(MPE_COLUMN_LEVEL_SECURITY) %conditional_export(MPE_COLUMN_LEVEL_SECURITY)
@@ -68,6 +82,7 @@ data MPE_CONFIG;
set &mpelib..MPE_CONFIG; set &mpelib..MPE_CONFIG;
where &dc_dttmtfmt. le tx_to; where &dc_dttmtfmt. le tx_to;
drop tx_: ; drop tx_: ;
if var_name='DC_MACROS' then var_value=tranwrd(var_value,"&mpelib","&newlib");
run; run;
%conditional_export(MPE_CONFIG) %conditional_export(MPE_CONFIG)
@@ -93,6 +108,7 @@ data MPE_EXCEL_CONFIG;
set &mpelib..MPE_EXCEL_CONFIG; set &mpelib..MPE_EXCEL_CONFIG;
where &dc_dttmtfmt. le tx_to; where &dc_dttmtfmt. le tx_to;
drop tx_: ; drop tx_: ;
if xl_libref="&mpelib" then xl_libref="&newlib";
run; run;
%conditional_export(MPE_EXCEL_CONFIG) %conditional_export(MPE_EXCEL_CONFIG)
@@ -107,6 +123,7 @@ data MPE_ROW_LEVEL_SECURITY;
set &mpelib..MPE_ROW_LEVEL_SECURITY; set &mpelib..MPE_ROW_LEVEL_SECURITY;
where &dc_dttmtfmt. le tx_to; where &dc_dttmtfmt. le tx_to;
drop tx_: ; drop tx_: ;
if rls_libref="&mpelib" then rls_libref="&newlib";
run; run;
%conditional_export(MPE_ROW_LEVEL_SECURITY) %conditional_export(MPE_ROW_LEVEL_SECURITY)
@@ -115,6 +132,7 @@ data MPE_SECURITY;
set &mpelib..MPE_SECURITY; set &mpelib..MPE_SECURITY;
where &dc_dttmtfmt. le TX_TO; where &dc_dttmtfmt. le TX_TO;
drop tx_: ; drop tx_: ;
if libref="&mpelib" then libref="&newlib";
run; run;
%conditional_export(MPE_SECURITY) %conditional_export(MPE_SECURITY)
@@ -142,6 +160,23 @@ data MPE_VALIDATIONS;
run; run;
%conditional_export(MPE_VALIDATIONS) %conditional_export(MPE_VALIDATIONS)
data MPE_XLMAP_INFO;
set &mpelib..MPE_XLMAP_INFO;
where &dc_dttmtfmt. le TX_TO;
drop tx_: ;
if XLMAP_TARGETLIBDS=:"&mpelib.." then
XLMAP_TARGETLIBDS=tranwrd(XLMAP_TARGETLIBDS,"&mpelib..","&newlib..");
run;
%conditional_export(MPE_XLMAP_INFO)
data MPE_XLMAP_RULES;
set &mpelib..MPE_XLMAP_RULES;
where &dc_dttmtfmt. le TX_TO;
drop tx_: ;
run;
%conditional_export(MPE_XLMAP_RULES)
/* finish up zip file */ /* finish up zip file */
ods package(ProdOutput) publish archive properties ods package(ProdOutput) publish archive properties
(archive_name="DCBACKUP.zip" archive_path="&work"); (archive_name="DCBACKUP.zip" archive_path="&work");

View File

@@ -84,7 +84,8 @@ data work.reject;
REVIEW_STATUS_ID="REJECTED"; REVIEW_STATUS_ID="REJECTED";
REVIEWED_BY_NM="&user"; REVIEWED_BY_NM="&user";
REVIEWED_ON_DTTM=&now; REVIEWED_ON_DTTM=&now;
REVIEW_REASON_TXT=symget('STP_REASON'); /* sanitise message to prevent code injection */
REVIEW_REASON_TXT=compress(symget('STP_REASON'), '&%;');
run; run;
%mp_lockanytable(LOCK, %mp_lockanytable(LOCK,

View File

@@ -51,6 +51,8 @@
data _null_; data _null_;
set work.sascontroltable; set work.sascontroltable;
call symputx('action',action); call symputx('action',action);
/* sanitise message to prevent code injection */
message=compress(message, '&%;');
call symputx('message',message); call symputx('message',message);
libds=upcase(libds); libds=upcase(libds);
call symputx('orig_libds',libds); call symputx('orig_libds',libds);

View File

@@ -16,6 +16,7 @@
@li mf_existfeature.sas @li mf_existfeature.sas
@li dc_assignlib.sas @li dc_assignlib.sas
@li mp_ds2cards.sas @li mp_ds2cards.sas
@li mp_ds2csv.sas
@li mp_abort.sas @li mp_abort.sas
@li mp_binarycopy.sas @li mp_binarycopy.sas
@li mp_cntlout.sas @li mp_cntlout.sas
@@ -117,10 +118,8 @@ options validvarname=upcase;
/* cannot proc export excel if PC Files is not licensed */ /* cannot proc export excel if PC Files is not licensed */
%then %do; %then %do;
%let outfile=%sysfunc(pathname(work))/&table..csv; %let outfile=%sysfunc(pathname(work))/&table..csv;
PROC EXPORT DATA= staged /* cannot use PROC EXPORT as we need to wrap all char values in quotes */
OUTFILE= "&outfile" %mp_ds2csv(work.staged,outfile="&outfile",headerformat=NAME)
DBMS=csv REPLACE;
RUN;
%let ext=csv; %let ext=csv;
%let mimetype=csv; %let mimetype=csv;
%end; %end;