Compare commits
38
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c18cb0f720 | ||
|
|
0d58be975d | ||
|
|
fe2388718a | ||
|
|
05b91cb43e | ||
|
|
072a133702 | ||
|
|
116c37b379 | ||
|
|
1fb4818d21 | ||
|
|
78e88a4112 | ||
|
|
bca1b9363f | ||
|
|
1b13120839 | ||
|
|
6cad23fce9 | ||
|
|
5fc0ff1ee3 | ||
|
|
5b70c000d7 | ||
|
|
d7cadc0b7b | ||
|
|
9914790777 | ||
|
|
e0ee99c178 | ||
|
|
5d183d1350 | ||
|
|
fa7a468fd3 | ||
|
|
d14395a0f8 | ||
|
|
f411b7d269 | ||
|
|
be8f445ae1 | ||
|
|
293022d46b | ||
|
|
60ec470404 | ||
|
|
ee3c8787ef | ||
|
|
5f443b0b16 | ||
|
|
3e8f200a1a | ||
|
|
03b67c44c7 | ||
|
|
4d462d310f | ||
|
|
1f1028a2a5 | ||
|
|
a07728be2b | ||
|
|
7d9df71fdb | ||
|
|
75b4acf1b1 | ||
|
|
73d25b1220 | ||
|
|
c4021a42ff | ||
|
|
698dea6d6b | ||
|
|
489819b051 | ||
|
|
0b7cf6a724 | ||
|
|
62f55db170 |
No files matched your search
@@ -181,7 +181,7 @@ jobs:
|
||||
sasjs request services/admin/makedata -t server-ci -d deploy/makedata.json -o ./makedata_out.json || true
|
||||
|
||||
- name: Prepare and run frontend and cypress
|
||||
timeout-minutes: 35
|
||||
timeout-minutes: 50
|
||||
run: |
|
||||
cd ./client
|
||||
mv ./cypress.env.example.json ./cypress.env.json
|
||||
@@ -196,9 +196,9 @@ jobs:
|
||||
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
|
||||
cat ./cypress.config.ts
|
||||
# Start frontend and run cypress
|
||||
# timeout 1800: SIGTERM after 30 min so Cypress can flush video/screenshots
|
||||
# timeout 2700: SIGTERM after 45 min so Cypress can flush video/screenshots
|
||||
# before the outer timeout-minutes hard-kills the step (avoids silent multi-hour hangs)
|
||||
npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && timeout 1800 npx cypress run --browser chrome --spec "cypress/e2e/csv-limited.cy.ts,cypress/e2e/hook-programs.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/filter-panel.cy.ts,cypress/e2e/licensing.cy.ts,cypress/e2e/viewer-labels.cy.ts,cypress/e2e/full-table-search.cy.ts,cypress/e2e/viewbox.cy.ts,cypress/e2e/stage.cy.ts,cypress/e2e/browser-info.cy.ts,cypress/e2e/ddl-export.cy.ts,cypress/e2e/row-cell-limits.cy.ts,cypress/e2e/derived-variables.cy.ts,cypress/e2e/startupservice-launcher.cy.ts"
|
||||
npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && timeout 2700 npx cypress run --browser chrome --spec "cypress/e2e/csv-limited.cy.ts,cypress/e2e/hook-programs.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/filter-panel.cy.ts,cypress/e2e/licensing.cy.ts,cypress/e2e/viewer-labels.cy.ts,cypress/e2e/full-table-search.cy.ts,cypress/e2e/viewbox.cy.ts,cypress/e2e/stage.cy.ts,cypress/e2e/browser-info.cy.ts,cypress/e2e/ddl-export.cy.ts,cypress/e2e/row-cell-limits.cy.ts,cypress/e2e/derived-variables.cy.ts,cypress/e2e/startupservice-launcher.cy.ts"
|
||||
|
||||
- name: Zip Cypress videos
|
||||
if: always()
|
||||
|
||||
@@ -193,11 +193,15 @@ jobs:
|
||||
# and its permissions are derived from this block.
|
||||
# contents: write -> push CHANGELOG.md / package.json commit + tag, and
|
||||
# create the Release via the Gitea API.
|
||||
# actions: write -> dispatch the Sites workflow for the new tag (that
|
||||
# route is scoped to the actions unit, which contents
|
||||
# does not grant).
|
||||
# The TSDOC_TOKEN and CODE_DATACONTROLLER_IO secrets continue to be used
|
||||
# because they target *different* servers (webdoc.datacontroller.io and
|
||||
# code.datacontroller.io), not the Gitea API.
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -401,3 +405,27 @@ jobs:
|
||||
done
|
||||
echo "Uploading SHA256SUMS ..."
|
||||
curl -k --fail-with-body -sS -H "$AUTH_HEADER" "$URL" -F "attachment=@SHA256SUMS"
|
||||
|
||||
- name: Publish the demo site for this release
|
||||
# The tag is created by this job's semantic-release step, so it carries
|
||||
# the Actions user as its author - and the forge suppresses ref events
|
||||
# from that user, which means `on: push: tags` in sites.yaml never fires.
|
||||
# Dispatch it instead: the API call is authenticated, and the run it
|
||||
# starts checks out the tag the same way.
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
AUTH_HEADER="Authorization: token ${GITEA_TOKEN}"
|
||||
BASE="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
||||
|
||||
TAG=$(curl -k --fail-with-body -sS -H "$AUTH_HEADER" "$BASE/releases/latest" | jq -r '.tag_name')
|
||||
echo "Dispatching Sites for ${TAG}"
|
||||
|
||||
# the dispatch endpoint resolves a bare name as a branch; a tag needs
|
||||
# the refs/tags/ prefix
|
||||
curl -k --fail-with-body -sS -X POST \
|
||||
-H "$AUTH_HEADER" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data "$(jq -n --arg ref "refs/tags/$TAG" '{ref: $ref}')" \
|
||||
"$BASE/actions/workflows/sites.yaml/dispatches"
|
||||
@@ -113,6 +113,39 @@ jobs:
|
||||
|
||||
- name: Build the browser-hosted site
|
||||
run: |
|
||||
# The published sites ship a working licence so they are usable beyond
|
||||
# the free tier. The pair is committed in sas/mocks/mock-licence.json
|
||||
# rather than minted here, so every deploy of both sites carries the
|
||||
# same key; the CI estate stays unlicensed because that file is outside
|
||||
# the deployed sync folder, so the specs still exercise the free tier.
|
||||
# The pair must round trip through the same WebCrypto the app uses,
|
||||
# because a committed pair can drift (one half regenerated, a value
|
||||
# truncated on copy) and nothing else would notice before publishing.
|
||||
# Identical values in the two fields is the HTTP-format marker the app
|
||||
# refuses in a secure context, so refuse to publish one.
|
||||
node -e "
|
||||
const d = require('./sas/mocks/mock-licence.json')
|
||||
const b = (s) => Buffer.from(String(s || ''), 'base64')
|
||||
if (!d.licenceKey || !d.activationKey || d.licenceKey === d.activationKey) {
|
||||
console.error('sas/mocks/mock-licence.json must hold an HTTPS-format key pair')
|
||||
process.exit(1)
|
||||
}
|
||||
crypto.subtle
|
||||
.importKey('pkcs8', b(d.activationKey), { name: 'RSA-OAEP', hash: 'SHA-256' }, false, ['decrypt'])
|
||||
.then((key) => crypto.subtle.decrypt({ name: 'RSA-OAEP', hash: 'SHA-256' }, key, b(d.licenceKey)))
|
||||
.then((plain) => {
|
||||
const data = JSON.parse(new TextDecoder().decode(plain))
|
||||
console.log('licence pair round trips: site', data.site_id, 'until', data.valid_until)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('sas/mocks/mock-licence.json does not decrypt:', e.message || e)
|
||||
process.exit(1)
|
||||
})
|
||||
"
|
||||
LICENCE_DIR="sasjs_root/drive/files$APP_LOC/mock-storage"
|
||||
mkdir -p "$LICENCE_DIR"
|
||||
cp sas/mocks/mock-licence.json "$LICENCE_DIR/licence.json"
|
||||
|
||||
# DC's data files exist only after makedata has run, so the drive is
|
||||
# captured first and shipped as the site's starting drive. services/web9
|
||||
# is the streamed frontend, which is already the site itself.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# sas/mocks/mock-licence.json holds a licence key pair for the mock estate: the
|
||||
# activation key is RSA key material the browser demo decrypts with, not a
|
||||
# credential for any service.
|
||||
sas/mocks/mock-licence.json:generic-api-key:2
|
||||
sas/mocks/mock-licence.json:generic-api-key:3
|
||||
+1
-1
@@ -135,7 +135,7 @@ After changing mock services, re-run `sasjs cbd -t server-ci` from `sas/mocks/sa
|
||||
|
||||
### Demo and staging sites
|
||||
|
||||
A push to `release/**` publishes [staging.datacontroller.io](https://staging.datacontroller.io); a release tag (`v*`, created by the release job after its tests pass) publishes [demo.datacontroller.io](https://demo.datacontroller.io) - workflow file `.gitea/workflows/sites.yaml`, using the `DEMO_DATACONTROLLER_IO` Surfer app password (user `allan`). Both are sites on one multi-site Surfer instance; the demo is its default site. The job builds the mocks as a browser-hosted site (SASjs CLI >= 6.1, `streamConfig.mockWeb`), so the services run in a service worker and the drive lives in the visitor's IndexedDB - there is no server. It runs `makedata` on a throwaway SASjs Server first and ships that drive as `sasjsbuild/mock-snapshot.json`; without it the site opens with no data. The mock ships no licence key, so both sites run on the free tier.
|
||||
A push to `release/**` publishes [staging.datacontroller.io](https://staging.datacontroller.io); a release tag (`v*`, created by the release job after its tests pass) publishes [demo.datacontroller.io](https://demo.datacontroller.io) - workflow file `.gitea/workflows/sites.yaml`, using the `DEMO_DATACONTROLLER_IO` Surfer app password (user `allan`). Both are sites on one multi-site Surfer instance; the demo is its default site. The job builds the mocks as a browser-hosted site (SASjs CLI >= 6.1, `streamConfig.mockWeb`), so the services run in a service worker and the drive lives in the visitor's IndexedDB - there is no server. It runs `makedata` on a throwaway SASjs Server first and ships that drive as `sasjsbuild/mock-snapshot.json`; without it the site opens with no data. The estate carries no licence key, so CI runs on the free tier until a key is registered through the licensing screen (`admin/registerkey`) - the Cypress setup does that for the specs that need one. The Sites job writes a licence into the published drive, so demo and staging are fully usable. The pair lives in `sas/mocks/mock-licence.json`: the two values are one RSA key and are regenerated together, and the `.gitleaksignore` entries for it are line-scoped, so inserting a line above the pair re-fires the scanner rather than revealing a new secret.
|
||||
|
||||
## GUI Elements
|
||||
|
||||
|
||||
@@ -40,8 +40,8 @@ context('special missings clip (DEMO_01)', function () {
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
|
||||
// The mock estate carries a valid licence key (mock-storage/licence.json),
|
||||
// so the app activates directly with no free-tier banner in shot.
|
||||
// Assumes the estate is already activated (the licensing spec registers a
|
||||
// valid key); otherwise a licensing page would sit in shot.
|
||||
cy.get('.nav-tree', { timeout: longerCommandTimeout }).should('exist')
|
||||
})
|
||||
|
||||
|
||||
@@ -75,6 +75,35 @@ context('derived variables: ', function () {
|
||||
|
||||
cy.screenshot('derived-3-filter-value', { capture: 'viewport' })
|
||||
})
|
||||
|
||||
it('4 | a derived column is marked read-only, and opens no editor', () => {
|
||||
openEditor('TESTDATA.DERIVED_DEMO')
|
||||
|
||||
// the editor marks a read-only column with this class, on the header and on
|
||||
// its cells (editor.component.ts, afterGetColHeader / afterGetCellMeta), so
|
||||
// this asserts the state rather than inferring it from something not
|
||||
// happening. STATE is the control: same grid, not read-only.
|
||||
headerCell('REGION').should('have.class', 'readonlyCell')
|
||||
headerCell('STATE').should('not.have.class', 'readonlyCell')
|
||||
|
||||
enterEditMode()
|
||||
|
||||
getCellByHeaderAndRow(0, 'REGION').dblclick({ force: true })
|
||||
cy.get('.handsontableInput').should('not.exist')
|
||||
getCellByHeaderAndRow(0, 'REGION').should('have.text', 'West')
|
||||
})
|
||||
|
||||
/**
|
||||
* The positive control for the case above: same screen, same edit mode, a
|
||||
* physical column - so its silence is about the column, not the screen.
|
||||
*/
|
||||
it('5 | a physical column in the same grid does accept an edit', () => {
|
||||
openEditor('TESTDATA.DERIVED_DEMO')
|
||||
enterEditMode()
|
||||
|
||||
getCellByHeaderAndRow(0, 'STATE').dblclick({ force: true })
|
||||
cy.get('.handsontableInput').should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -103,6 +132,23 @@ const openEditor = (libMem: string) => {
|
||||
cy.wait(2500)
|
||||
}
|
||||
|
||||
/** The EDIT screen opens read-only; this is the button that starts an edit. */
|
||||
const enterEditMode = () => {
|
||||
cy.get('.btnCtrl button.btn-primary', { timeout: longerCommandTimeout })
|
||||
.click()
|
||||
.then(() => cy.wait(2500))
|
||||
}
|
||||
|
||||
/** The grid header cell for a column, by its rendered text. */
|
||||
const headerCell = (headerText: string) =>
|
||||
cy.get('.ht_clone_top .htCore thead tr th').then(($ths) => {
|
||||
const th = [...$ths].find(
|
||||
(candidate) => (candidate as HTMLElement).innerText.trim() === headerText
|
||||
)
|
||||
if (!th) throw new Error(`header not found: ${headerText}`)
|
||||
return cy.wrap(th)
|
||||
})
|
||||
|
||||
const openFilterPopup = () => {
|
||||
cy.contains('button.btnView', 'Filter', {
|
||||
timeout: longerCommandTimeout
|
||||
|
||||
@@ -79,14 +79,34 @@ Cypress.Commands.add(
|
||||
.then(() => {
|
||||
cy.wait(2000)
|
||||
|
||||
if ($body.find('.nav-tree').length > 0) {
|
||||
/**
|
||||
* If licence key is already working, then skip rest of the function
|
||||
* A key that is already working needs no update. Ask the loaded
|
||||
* page, not the tree: a free tier estate renders the nav tree as
|
||||
* well, so the tree cannot tell a licensed estate from an
|
||||
* unlicensed one - the licence banner is what does. Reading the
|
||||
* body again here also matters on an estate that shows a login
|
||||
* form, where the body captured above has no tree yet.
|
||||
*/
|
||||
cy.get('body').then(($loaded) => {
|
||||
const licenceProblem = $loaded.find('#demo-banner').length > 0
|
||||
|
||||
if (!licenceProblem && $loaded.find('.nav-tree').length > 0) {
|
||||
return logout(() => {
|
||||
return
|
||||
})
|
||||
} else {
|
||||
}
|
||||
|
||||
/**
|
||||
* An estate with no key is left as it is unless the caller asked
|
||||
* for a licensed one - the free tier specs are written against
|
||||
* that state and must not have a key planted under them.
|
||||
*/
|
||||
if (!forceLicenceKey) {
|
||||
return logout(() => {
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
const keyData = {
|
||||
valid_until: moment().add(20, 'day').format('YYYY-MM-DD'),
|
||||
number_of_users: 10,
|
||||
@@ -106,11 +126,6 @@ Cypress.Commands.add(
|
||||
cy.wait(20000)
|
||||
}
|
||||
|
||||
if (!forceLicenceKey) {
|
||||
return logout(() => {
|
||||
return
|
||||
})
|
||||
} else {
|
||||
return updateLicenseKeyQuick(keysGen, () => {
|
||||
cy.wait(25000)
|
||||
return acceptTermsIfPresented((result: boolean) => {
|
||||
@@ -122,11 +137,10 @@ Cypress.Commands.add(
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ const check = (cwd) => {
|
||||
onlyAllow:
|
||||
'AFLv2.1;Apache 2.0;Apache-2.0;Apache*;Artistic-2.0;BlueOak-1.0.0;0BSD;BSD*;BSD-2-Clause;BSD-3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;MPL-2.0;ODC-By-1.0;Python-2.0;Unlicense;',
|
||||
excludePackages:
|
||||
'@cds/city@1.1.0;@handsontable/angular-wrapper@16.0.1;@handsontable/angular-wrapper@17.1.0;@handsontable/angular-wrapper@18.0.0;@handsontable/angular-wrapper@18.1.1;handsontable@^16.0.1;handsontable@16.2.0;handsontable@17.1.0;handsontable@18.0.0;handsontable@18.1.1;hyperformula@2.7.1;hyperformula@3.0.0;hyperformula@3.1.0;hyperformula@3.2.0;hyperformula@3.3.0;jackspeak@3.4.3;path-scurry@1.11.1;package-json-from-dist@1.0.1;buffers@0.1.1'
|
||||
'@cds/city@1.1.0;@handsontable/angular-wrapper@16.0.1;@handsontable/angular-wrapper@17.1.0;@handsontable/angular-wrapper@18.0.0;@handsontable/angular-wrapper@18.1.1;@handsontable/angular-wrapper@18.1.2;handsontable@^16.0.1;handsontable@16.2.0;handsontable@17.1.0;handsontable@18.0.0;handsontable@18.1.1;handsontable@18.1.2;hyperformula@2.7.1;hyperformula@3.0.0;hyperformula@3.1.0;hyperformula@3.2.0;hyperformula@3.3.0;jackspeak@3.4.3;path-scurry@1.11.1;package-json-from-dist@1.0.1;buffers@0.1.1'
|
||||
},
|
||||
(error, json) => {
|
||||
if (error) {
|
||||
|
||||
Generated
+54
-80
@@ -20,7 +20,7 @@
|
||||
"@clr/angular": "file:libraries/clr-angular-17.9.0.tgz",
|
||||
"@clr/icons": "^13.0.2",
|
||||
"@clr/ui": "file:libraries/clr-ui-17.9.0.tgz",
|
||||
"@handsontable/angular-wrapper": "18.1.1",
|
||||
"@handsontable/angular-wrapper": "18.1.2",
|
||||
"@sasjs/adapter": "5.1.2",
|
||||
"@sasjs/utils": "4.0.0",
|
||||
"@sheet/crypto": "file:libraries/sheet-crypto.tgz",
|
||||
@@ -35,7 +35,7 @@
|
||||
"d3-graphviz": "^5.0.2",
|
||||
"exceljs": "^4.4.0",
|
||||
"fs-extra": "^7.0.1",
|
||||
"handsontable": "18.1.1",
|
||||
"handsontable": "18.1.2",
|
||||
"https-browserify": "1.0.0",
|
||||
"hyperformula": "^2.5.0",
|
||||
"iconv-lite": "^0.5.0",
|
||||
@@ -969,7 +969,6 @@
|
||||
"integrity": "sha512-pQiOg+se1AU/ncMlnJ9V6xYnMQ84qI1BGWuJpbU6A99VTXJg90scg0+T7DWmKssR1YjP5qmmBtrZfKsHEcLW/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@angular-eslint/bundled-angular-compiler": "19.8.1",
|
||||
"eslint-scope": "^8.0.2"
|
||||
@@ -1000,7 +999,6 @@
|
||||
"integrity": "sha512-HdDfbLwpxzx49wAWz4oOUQKSkZcFNRpWEGsDEgufZ3ExwxRk1/2QPJuvZ2CbhKeEcLPrLKUorQ8XlGrNsJmNiQ==",
|
||||
"deprecated": "@angular/animations is deprecated. Use `animate.enter` and `animate.leave` instead. For more information see: https://v22.angular.dev/guide/animations.",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -1146,7 +1144,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.14.tgz",
|
||||
"integrity": "sha512-7bZxc01URbiPiIBWThQ69XwOxVduqEKN4PhpbF2AAyfMc/W8Hcr4VoIJOwL0O1Nkq5beS8pCAqoOeIgFyXd/kg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"parse5": "^8.0.0",
|
||||
"tslib": "^2.3.0"
|
||||
@@ -1443,7 +1440,6 @@
|
||||
"integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@inquirer/checkbox": "^4.2.1",
|
||||
"@inquirer/confirm": "^5.1.14",
|
||||
@@ -1586,7 +1582,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.34.tgz",
|
||||
"integrity": "sha512-OhB/l0cNldOvQeICqbWqRUzK5fF6zNYy44Q4cIGJZql8HXL/invGiqWLE9f+kU8z4jRisofHIGTBtyJQYYOudg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -1603,7 +1598,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.34.tgz",
|
||||
"integrity": "sha512-8agPW384c/JER1ersPTmJJfcCOnlYcBbdj0MYRxNdiYe+7gtnspF3ORuJmxqCEpMS17AljSmk2skwUcYnHB6xA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -1617,7 +1611,6 @@
|
||||
"integrity": "sha512-PKHzhSOyDUFh64Ull3fUwgFlxnsQNEAuRyz4KqQ8FsTGZ4pOpUaZntO+2Xn1nX/gT2whSyGBJhD3H37zA8ZGEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "7.29.7",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14",
|
||||
@@ -1650,7 +1643,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.34.tgz",
|
||||
"integrity": "sha512-xOvSmGoao3SVPz//R7zTrq7+WdrIGi+WNpAdxJEvJJXuSpLGS3kSUqukjY4kOMochg2v0NCEPIdCyH+yPs8JSw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -1694,7 +1686,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.34.tgz",
|
||||
"integrity": "sha512-0bd2Bj05WPoBTFM0A13IwbzxKVQPstx/N+7X0ZWKCifliyydIc7DBHu+r37ybJpl8ZsCVBe7DoR9Gon4lZ+zlw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -1790,7 +1781,6 @@
|
||||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
@@ -2317,6 +2307,7 @@
|
||||
"integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.29.7",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.29.7"
|
||||
@@ -3362,6 +3353,7 @@
|
||||
"integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.29.7",
|
||||
"@babel/helper-compilation-targets": "^7.29.7",
|
||||
@@ -3448,6 +3440,7 @@
|
||||
"integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.29.7",
|
||||
"@babel/helper-remap-async-to-generator": "^7.29.7",
|
||||
@@ -3466,6 +3459,7 @@
|
||||
"integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.29.7",
|
||||
"@babel/helper-plugin-utils": "^7.29.7",
|
||||
@@ -3484,6 +3478,7 @@
|
||||
"integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-define-polyfill-provider": "^0.6.8",
|
||||
"core-js-compat": "^3.48.0"
|
||||
@@ -3498,6 +3493,7 @@
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
@@ -3604,7 +3600,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@cds/core/-/core-6.17.0.tgz",
|
||||
"integrity": "sha512-afG+KwNE4BDc6CAylgGbJrJUeGYFbOIfagylaqqbS7fgIX2h3ZqFcUOv/2hujSZ9GZiuCSa+A/BkJi7K83QEgg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"lit": "^2.1.3",
|
||||
"ramda": "^0.29.0",
|
||||
@@ -3786,7 +3781,6 @@
|
||||
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"readdirp": "^5.0.0"
|
||||
},
|
||||
@@ -4904,9 +4898,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@handsontable/angular-wrapper": {
|
||||
"version": "18.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@handsontable/angular-wrapper/-/angular-wrapper-18.1.1.tgz",
|
||||
"integrity": "sha512-paiINuWOevybN6RFUrtSOgRv5SaFlMd5dRbBOzeLyJJzqhLW7Jg+QSy6+51FAL6FKHZPfWHYoP0F2YgNPEXYbg==",
|
||||
"version": "18.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@handsontable/angular-wrapper/-/angular-wrapper-18.1.2.tgz",
|
||||
"integrity": "sha512-LoUc3jIO7Y5rkSzHW+njvQWA6x8RipkWmBkR7vmnEqgxcytejgd7RIy6/b3O0AqYuv2XUT+vXrNacn9wEekWhQ==",
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"optionalDependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
@@ -7176,7 +7170,6 @@
|
||||
"integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
@@ -7200,7 +7193,6 @@
|
||||
"integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
@@ -7214,7 +7206,6 @@
|
||||
"integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/semantic-conventions": "1.28.0"
|
||||
},
|
||||
@@ -7241,7 +7232,6 @@
|
||||
"integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/api-logs": "0.57.2",
|
||||
"@types/shimmer": "^1.2.0",
|
||||
@@ -7672,7 +7662,6 @@
|
||||
"integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "1.30.1",
|
||||
"@opentelemetry/semantic-conventions": "1.28.0"
|
||||
@@ -7700,7 +7689,6 @@
|
||||
"integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "1.30.1",
|
||||
"@opentelemetry/resources": "1.30.1",
|
||||
@@ -7729,7 +7717,6 @@
|
||||
"integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
@@ -9377,7 +9364,8 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz",
|
||||
"integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/http-errors": {
|
||||
"version": "2.0.5",
|
||||
@@ -9721,7 +9709,6 @@
|
||||
"integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.65.0",
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
@@ -9895,7 +9882,6 @@
|
||||
"integrity": "sha512-Adfoy59kxNEjT5E5fnyrp7GpHYuWs8ONbdohu4ez6IU46qDq8LltG90tLmkucZB2uhYL8mqBjtXqGd8yhfjXqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
@@ -9990,6 +9976,7 @@
|
||||
"integrity": "sha512-bzHngBbmIgzj6TsTjVUCntGx6FFLgE7mPjpO0PzHycOaZHXqS43XnrkFIHRS16LLr/xO6FHSXG3GJFo/F4bSBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.72.0",
|
||||
"@typescript-eslint/types": "^8.72.0",
|
||||
@@ -10012,6 +9999,7 @@
|
||||
"integrity": "sha512-JewPyJOYcXBSAS7PIgufscHOwZqhTGyBRiQJhhuDln66tMchxvvt7nJ90JKKIoOelmdHA3kAQbm9cEUHc4JSEA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.72.0",
|
||||
"@typescript-eslint/visitor-keys": "8.72.0"
|
||||
@@ -10030,6 +10018,7 @@
|
||||
"integrity": "sha512-0vbxCf7xLVPSjcOdm6eQbgViH8EDJ8xnl1laLT9yoqSz2YmcSfSg5Vdl0JUvZf6rcuJ0ZivJIuMD4edTg8obaA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
@@ -10047,6 +10036,7 @@
|
||||
"integrity": "sha512-gwLfFgWcw15tN2KXDCJvxEOQjPn+P3j/n3eSqkZ5sX4VpW9Cosodr+tWwBZRvJT3TcKx6HnPRGZMCNAiP1Xhug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.72.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.72.0",
|
||||
@@ -10075,6 +10065,7 @@
|
||||
"integrity": "sha512-E2mroviasaWecDzTGiGMtuEguuLVOeRggnrIn93CJuxYJzQBHbyymlT08aVQydbBal5538o+JkBKXh2sRuCyIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.72.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
@@ -10093,6 +10084,7 @@
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
@@ -10106,6 +10098,7 @@
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
@@ -10398,7 +10391,6 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
|
||||
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -10469,7 +10461,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
|
||||
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -10933,7 +10924,6 @@
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz",
|
||||
"integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.6",
|
||||
@@ -11069,7 +11059,6 @@
|
||||
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz",
|
||||
"integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"bare-abort-controller": "*"
|
||||
},
|
||||
@@ -11441,7 +11430,8 @@
|
||||
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
|
||||
"integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
"license": "ISC",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/browserify-aes": {
|
||||
"version": "1.2.0",
|
||||
@@ -11499,7 +11489,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.11.12",
|
||||
"caniuse-lite": "^1.0.30001809",
|
||||
@@ -11873,7 +11862,6 @@
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
@@ -12305,7 +12293,8 @@
|
||||
"resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz",
|
||||
"integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
"license": "ISC",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/common-tags": {
|
||||
"version": "1.8.2",
|
||||
@@ -12986,7 +12975,6 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@cypress/request": "^4.0.0",
|
||||
"@cypress/xvfb": "^1.2.4",
|
||||
@@ -13388,7 +13376,6 @@
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -13664,8 +13651,7 @@
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1625959.tgz",
|
||||
"integrity": "sha512-wRBSU330hwOLLcb3N4NIe3eFs6MgT6ku3AiZONjnTSJ7f3dVchJfn6nE0Lfos9jK1na15bgp7xLhaCx40Y47NQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/dezalgo": {
|
||||
"version": "1.0.4",
|
||||
@@ -13691,6 +13677,7 @@
|
||||
"integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
@@ -14311,7 +14298,6 @@
|
||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
@@ -14757,7 +14743,6 @@
|
||||
"integrity": "sha512-Bdcs4+3qlpVlx2NRn6fgX2Ue2/gGRaPeawebgclM0ERSCqDpA+owF1fdPwjJUTAJWMTuAaxjDf+hzb0/4eKvvw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
@@ -15185,6 +15170,7 @@
|
||||
"integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"common-path-prefix": "^3.0.0",
|
||||
"pkg-dir": "^7.0.0"
|
||||
@@ -15219,6 +15205,7 @@
|
||||
"integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -15744,11 +15731,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/handsontable": {
|
||||
"version": "18.1.1",
|
||||
"resolved": "https://registry.npmjs.org/handsontable/-/handsontable-18.1.1.tgz",
|
||||
"integrity": "sha512-8vz/jp96m0aJQPRepOBoImsC/bLe9mE/O4nOB6mll9DCjN5nqXHX9LV4zu/XDpD89TKJwtA+TNhVUkFQxW+q7w==",
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"peer": true
|
||||
"version": "18.1.2",
|
||||
"resolved": "https://registry.npmjs.org/handsontable/-/handsontable-18.1.2.tgz",
|
||||
"integrity": "sha512-iL4Cia0Fsl+md1oCqUS8PJqfft3583d5ptc5M7ewfp6JTUsNsrIcpF3GPYvZy57e7P5PACX8WboGlkwgHn0h9Q==",
|
||||
"license": "SEE LICENSE IN LICENSE.txt"
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
@@ -15926,7 +15912,6 @@
|
||||
"integrity": "sha512-6E2QDAc9Ick9Sq77ZrGS/dk2WUYni91aufTw6LJKpV7w8kW5/GxVUc650FOADOmlwg3K+f7Pun6XlV+pYmW6gw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
@@ -17124,8 +17109,7 @@
|
||||
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.2.tgz",
|
||||
"integrity": "sha512-2oIUMGn00FdUiqz6epiiJr7xcFyNYj3rDcfmnzfkBnHyBQ3cBQUs4mmyGsOb7TTLb9kxk7dBcmEmqhDKkBoDyA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jest-worker": {
|
||||
"version": "27.5.1",
|
||||
@@ -17162,7 +17146,6 @@
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
@@ -17449,7 +17432,6 @@
|
||||
"integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@colors/colors": "1.5.0",
|
||||
"body-parser": "^1.19.0",
|
||||
@@ -18005,7 +17987,6 @@
|
||||
"integrity": "sha512-umRhrCH7fCi8Uj2RcwKjJdvUORTjeWqkdKx0LbcZvjIwsAVsnIAGcxHaqowPeBFBjQuWOeC/bve0AlpFzF/+SQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"copy-anything": "^3.0.5",
|
||||
"parse-node-version": "^1.0.1"
|
||||
@@ -18467,7 +18448,6 @@
|
||||
"integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cli-truncate": "^4.0.0",
|
||||
"colorette": "^2.0.20",
|
||||
@@ -19594,6 +19574,7 @@
|
||||
"integrity": "sha512-beammieDdiWdkjANWeWPpkxUROYGi5FN88fPOkLiticz7HTg3Y+Pho3EYE6SUdcuWnZ3h2WD8EXCA55JF4o6wg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"browser-stdout": "^1.3.1",
|
||||
"chokidar": "^5.0.0",
|
||||
@@ -19634,7 +19615,8 @@
|
||||
"url": "https://github.com/sponsors/nodeca"
|
||||
}
|
||||
],
|
||||
"license": "PSF-2.0"
|
||||
"license": "PSF-2.0",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/mocha/node_modules/chokidar": {
|
||||
"version": "5.0.0",
|
||||
@@ -19642,6 +19624,7 @@
|
||||
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"readdirp": "^5.0.0"
|
||||
},
|
||||
@@ -19658,6 +19641,7 @@
|
||||
"integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"minimatch": "^10.2.2",
|
||||
"minipass": "^7.1.3",
|
||||
@@ -19676,6 +19660,7 @@
|
||||
"integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -19699,6 +19684,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"argparse": "^3.0.3"
|
||||
},
|
||||
@@ -19712,6 +19698,7 @@
|
||||
"integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
@@ -19726,6 +19713,7 @@
|
||||
"integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
@@ -19739,6 +19727,7 @@
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
@@ -21446,6 +21435,7 @@
|
||||
"integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"find-up": "^6.3.0"
|
||||
},
|
||||
@@ -21462,6 +21452,7 @@
|
||||
"integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"locate-path": "^7.1.0",
|
||||
"path-exists": "^5.0.0"
|
||||
@@ -21479,6 +21470,7 @@
|
||||
"integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"p-locate": "^6.0.0"
|
||||
},
|
||||
@@ -21495,6 +21487,7 @@
|
||||
"integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"yocto-queue": "^1.0.0"
|
||||
},
|
||||
@@ -21511,6 +21504,7 @@
|
||||
"integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"p-limit": "^4.0.0"
|
||||
},
|
||||
@@ -21527,6 +21521,7 @@
|
||||
"integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
}
|
||||
@@ -21537,6 +21532,7 @@
|
||||
"integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
},
|
||||
@@ -21609,7 +21605,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.16",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -22845,7 +22840,6 @@
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
@@ -22900,7 +22894,6 @@
|
||||
"integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
@@ -24209,7 +24202,6 @@
|
||||
"integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.14.0",
|
||||
@@ -24438,7 +24430,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
|
||||
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tldts": "^6.1.32"
|
||||
},
|
||||
@@ -24701,8 +24692,7 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsyringe": {
|
||||
"version": "4.10.0",
|
||||
@@ -24869,7 +24859,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -24925,19 +24914,10 @@
|
||||
"integrity": "sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "8.9.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
|
||||
"integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
|
||||
@@ -25127,7 +25107,6 @@
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
@@ -25251,7 +25230,6 @@
|
||||
"integrity": "sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0 || ^0.28.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -25442,7 +25420,6 @@
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.112.0.tgz",
|
||||
"integrity": "sha512-NIBpsRuCll036haDDt/CfcTDfsWNR9Z782nMRMI9D377BsGcYzZDTNU5TyzhCJzPIcihAxMBaMAhq2WSaDrH8Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.8",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
@@ -25514,7 +25491,6 @@
|
||||
"integrity": "sha512-HNLRmamRvVavZQ+avceZifmv8hmdUjg43t6MI4SqJDwFdW7RPQwH5vzGhDRZSX59SgfbeHhLnq3g+uooWo7pVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/bonjour": "^3.5.13",
|
||||
"@types/connect-history-api-fallback": "^1.5.4",
|
||||
@@ -25906,7 +25882,8 @@
|
||||
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-10.0.3.tgz",
|
||||
"integrity": "sha512-6z2Iis68Wqth93/G/wJP9u+R3O+d2XTlgWChGCwuT1qLbBsOYueGRZuJ++v3mtDP5KjYdy+WzvWC+VWETSVXJA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
"license": "Apache-2.0",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "9.0.2",
|
||||
@@ -26197,7 +26174,6 @@
|
||||
"integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"pend": "~1.2.0"
|
||||
},
|
||||
@@ -26314,7 +26290,6 @@
|
||||
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
@@ -26333,8 +26308,7 @@
|
||||
"version": "0.15.1",
|
||||
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz",
|
||||
"integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -54,7 +54,7 @@
|
||||
"@clr/angular": "file:libraries/clr-angular-17.9.0.tgz",
|
||||
"@clr/icons": "^13.0.2",
|
||||
"@clr/ui": "file:libraries/clr-ui-17.9.0.tgz",
|
||||
"@handsontable/angular-wrapper": "18.1.1",
|
||||
"@handsontable/angular-wrapper": "18.1.2",
|
||||
"@sasjs/adapter": "5.1.2",
|
||||
"@sasjs/utils": "4.0.0",
|
||||
"@sheet/crypto": "file:libraries/sheet-crypto.tgz",
|
||||
@@ -69,7 +69,7 @@
|
||||
"d3-graphviz": "^5.0.2",
|
||||
"exceljs": "^4.4.0",
|
||||
"fs-extra": "^7.0.1",
|
||||
"handsontable": "18.1.1",
|
||||
"handsontable": "18.1.2",
|
||||
"https-browserify": "1.0.0",
|
||||
"hyperformula": "^2.5.0",
|
||||
"iconv-lite": "^0.5.0",
|
||||
|
||||
@@ -868,6 +868,21 @@ export class MultiDatasetComponent implements OnInit, AfterViewInit {
|
||||
return this.sasStoreService
|
||||
.callService(myParams, 'SASControlTable', 'editors/getdata', libds)
|
||||
.then((res: EditorsGetDataServiceResponse) => {
|
||||
// getdata serves a selection over DC_MAXOBS_WEBEDIT / DC_MAXCELLS_WEBEDIT
|
||||
// truncated, rather than aborting the way it used to. This flow compares
|
||||
// the whole table against the upload, so a partial read would compute
|
||||
// the changes against the wrong starting state - refuse the table
|
||||
// instead of submitting differences nobody asked for.
|
||||
const sasParams = res?.data?.sasparams?.[0]
|
||||
|
||||
if (sasParams && Number(sasParams.TRUNCATED) === 1) {
|
||||
this.eventService.showInfoModal(
|
||||
'Upload error',
|
||||
`${libds} holds more than the EDIT limit allows (${sasParams.TRUNC_MSG}), so it cannot be compared with the upload. Raise DC_MAXOBS_WEBEDIT / DC_MAXCELLS_WEBEDIT, or submit the table without the multi-table upload.`
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
return res
|
||||
})
|
||||
.catch((err: any) => {
|
||||
|
||||
@@ -186,7 +186,8 @@ describe('SasService openServiceInNewWindow', () => {
|
||||
|
||||
expect(openSpy).toHaveBeenCalledWith(
|
||||
'https://sas.example.com/SASJobExecution/?_program=/Public/app/dc/services/public/startupservice&_contextname=ctx&_debug=131',
|
||||
'_blank'
|
||||
'_blank',
|
||||
'noopener'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -199,7 +200,8 @@ describe('SasService openServiceInNewWindow', () => {
|
||||
|
||||
expect(openSpy).toHaveBeenCalledWith(
|
||||
'https://sas.example.com/SASJobExecution/?_program=/Public/app/dc/services/public/startupservice&_debug=131',
|
||||
'_blank'
|
||||
'_blank',
|
||||
'noopener'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -867,7 +867,10 @@ export class SasService {
|
||||
params
|
||||
})
|
||||
|
||||
window.open(url, '_blank')
|
||||
// noopener: the SAS log page is on a different origin (Viya's serverUrl is
|
||||
// not the app's), and without it the opened window keeps a handle on
|
||||
// window.opener and can navigate this tab.
|
||||
window.open(url, '_blank', 'noopener')
|
||||
}
|
||||
|
||||
public getServerType(): string {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"licenceKey": "G4H/DNTdEMkmCIxOrYkRYTQ1P4sWg0gs8Zk9rakxFNYDZcLyEPMJJo82YJQ/S24P2E/YPxaWsDZXgOrWKfeCDS7NrLcwsoFNa8KRF2WTlRgSMiG4sjBRX8zNEIvszj5DAq4LEgOUUAdfctNfpUcygbxT1dhqy6gU3hBu/xRuXGd36rhxQwfahaKkfaCg/rFplkrvqnjoJ6622BxWP/HG1dxZUE7HNJyT4tNaXzlC9U6u/aaJJ66+V+9511BwjM9ss4nyHuHoogVE1yrzLMlL94P3QoM1hwkGHnEHmFzg3zHbQG1f3pUs9YzK0ytsFKU7AK948gJrGbf37Zqa+Mz7EZf6XzqqXx0zQ4y9zAhLV9s4QG8JdhrByhfG5kefrs4L/BpgKq3ElgYL5EMkrLJUrlqWYI2ouxJuIGCQz41VCWXJ2S7pd39ljifc8Zj+t2Mn5EEkRVsrbdxjCkyoZOQ1ALofa1+auUI26Rm5rwEgrfhEocDmphJpO2F8lZgwDxbepSE6v/vEV/JQWBQTyrX10W+qOEu8OhvZrYJo+eIJMaO7q1pjfdrn3XhoH+BeB3Sy1sevGLXQZC+CzOtcvajetGGCDAabtTLc49ouWPUiUOAWPE3uYymsMoRZgpLIMxaUM86hoK0r9T6wSaP0bddK754sXN1jWwIDAHSGz5WO1ko=",
|
||||
"activationKey": "MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQD2w8Ge6tMI82vaQ7x5ZdlwnTJycn0UFZoSgRwObaM0NC5wp9Y8lwkVzN8FUhg2oYSTpvyJpix+VGRj0AlNBwFAVcJG9VDPYh2LE3Us9ghRfkzFUiM2PqorHnzL5DwpailRtmFHoDQPGwwSmKncTo2QHql/K6tWe7g3Nv9uqQNks283hvDQ6PVGU4TaFTSRbjGX4j+JAR651J6wI6nY+9kNxOBwnL/yKTnJGmL8oqZ+oC45quZzgRW4dLIeTODcB4IZe2ruxZ9tvXpSuAESiOv5bBZyMJEKdKwsRdt6vk+bezkZBCzPq+ly892Ncq7x5u4/8dGcGxRxxp5TnIIx3hFoiXZ7svXJ7st6uHuKkuwYq+ebWh6bcvQAwH2wCP40twMjQgmU8nHHo6+pulxu1wVZBxtoyM4m3Lu0ZncTi6mF0duiPvpkNNAZzKOF98aUq3hxYp5PP28qrtWuPsgSdTNWJwnV2R288yAPx4WLUAGB9h889d/m67HUiKadmfG4oyUnChCU/mqeJ/5nQkK1n76ezRPGsTcMZIv2mmPLShA/j38lGMjR/3pzM2Ha6UFjJKpurpeDiyt5pUdmRBpQRoBUx/PHnbXBsO9PBXa0mjdq3UFbgb5iBD1Fz2YClZ/h0d4YlYUMSeharDrLfR15g3fGnzxrkR03zYeHkxMUi5vYHQIDAQABAoICAFxUg5pcH8lOF1FMHfXIY5MM5mbHKsC8knERCPfnEvVB/wxc6D60fow/I0Vg4blzNuFuTjiMykIcfpGQMiK1l8TEMdSODoYCHq6+gY3ootUnLj94i79MtCw9FJHPnKkcVUiDNqPvr/rk8l/06j2J0NMXffEc3FeYJCXYuLtlluTzdI4wueNY/wkoz+EvpQMzWXAMkhxHpIEAqr2aIn+TQDQ0MYxBQlqwDTSPYHFJxIVFlvYx2VdnecKSO2E1/76kOowhTzH3FL+hcWcI/TG7tJuXU1C3llV0sUVBgC1NjwjMkuEm3EN5/ffqgj/eJNJTR1r6tqSzxzMJD42WVmqlPhqEmmRE1yeaio6Ulm5EJLREYEY4/1EotdIO6PCXF1Xeo+10VJLcwi19vzMlz7vFB6vSJzJNYOaiwm4rgmF12+nNt18cHoFvegGPAoCJurUhXU+iV6LlKYB8J/+kpITleZwm4dEaa6EWxgFf4nZ4N+qRVqk4JfC+rN3DQgbPcII+z7axAb8xutq5rvzskGAmKcTEagMG+pnk9DI+0m1C2J6+mC/YruVlpSlPB7Ecu/OF++vJoSCLuDXVuxMTQ3tQXoicOG/S8d1aa3nq4fvLIgZTOZoffUvNvjCyy/Dlv6vaDVCjgqt2kFZVSZv3C6ttyrNVBSjhucASGTq/7q82JKVrAoIBAQD7e7SYqDM8SAjOfpRAHCbIVVSaHw1tul16B6JAF67OIs1uD//Y+maL4GHlpFxnib0tg6z3+sG10NMUhK7elB6SkJ8voT+Ztg/+on659vmaqwTlkGjgks+8DmIw1b0ZbXuIqhnkOeSsll2V9NWItIrbzNwvhtMEQsOtFvmrLlKQvx9Bo3lGmbfdU1/9XpwW4wiA5ONcFCl1xllX9nItB+ActPoAeQgnuejWAISQkITDTOFcGu0AjMjJ66HUlY/WDbf7wY/s2aMbOiVx7B6YzQnK+lZdjjNfoaL8nfN8Z8ZzJFAgmTQ9ztZCJ8fLTfZDsYD6XZnVEIG401+6de0Ea4MHAoIBAQD7Mlr++keNDxYy3k6kwTXDOSKMDfXIDverFy/tiNorcVU5nIpuhjowl4HIQVAhzlXhATp938LM/PvJIeu9HtgsY9BFNL52fr7iGEWknqZQVE+8Um35tzV3vtPH9NVXHIYmcRCAQQJ5a6oVQRJKAM3E1+htM2AeZKMaQSCC6VHGBXJG9nEltdMKTet09pd26Z5y/YIfTqbIekyjRomGdInyWBkpJLJ/uROVb9OGnsYWadDktn7y9+oH+VZoJxW5QwXV0OL7wMzymlrKhhBNMziJRBk4rVztclYzUShpwE8Xfjt5WYkOoieXtmkdpF7TMvRYy6cdflh39l8s7THq4U67AoIBAQDPShR+kIZK9gVsUCHbBllhYBSwRVBIVgmfAJPYyKyp9T3nrwBw1VWZjoajR2tzXLG2xobYkLUDLPIQQ53wkZ7xqIvOMte917gvZ7mVh+IOf1E1+Ga6VcktdZ2yKzwHrZrZ93wIB0BakkbpyDoKyOZ8bsw/0J43MOEh14JMe45LCX62GA1rCGHp6UOCEW4P32uu8vwFdgkGr0wNmXJbc9ZjUzmPEjSKqk8ltp+OmdEVOlW4GempPmJzLeri07UP95bjyZTc4jfzxWYAFWTTBo8/5EE0NHZcKVuzZm7A6LlHR3NUq3PITqmFNk0VY1nOSo3J0UVvvn9u3axWgcWwkv4ZAoIBAQD5GaB/RfZ6fcGHxEi8vRTxedE2jglz0NPuCFpw9ctUVVksfn4QyG8gDL6YtZwfShyDutIkTSdCXIXbmRXLP7Ejo/+/AWT24Q2t7F/ztZg8BL/lguCoR/RvwP8HRVXiwtiNeoXgiZdIjWnnFdZrKIzlbS6CS/Y/bZEI0hjJeVKsSm5ezLi7qqmTiF28aWUeeOG2KotFPJvMJuuk5qgue5boilC5KjkQy5oOEQz0bLMHOvv6MreFfi/IpIv5UPkL9NjuhezxtuoGkucB+xxjUi34gV15hQGoPkAZN5wZ8qj8JSQZXJ67sjYNDskCqiOW0Orau2oHp2lX3SvYw4dC4xdNAoIBAQDSXF8OnWu08smGDs4S57yfqe59N0ULIBDOZdEQcLXRaDeAoOwQj8szUf0pycXUZLvXxv29iF61FJg70iGGCp3lW+9Oe0P+dGsKvzphRGsQPZy4h/qZDoQ9dE667ejI4aDb/C1GaOZ8TT1QfpQcVKbgLjdC19IGQPhFljGM1wMQojU5lp4/iKmA3J3ahOuV4dvEUr6xkqzq+PhH405XdURXsVc1J6qYX2wQXRoB0oDsrCbqidZi14DaeE8UhZRA6m9haUOb/3Y+OkCs2QIW4MNZ+zg8XtHjiUhMxAaJ6zdEFltWPoVFsVfgoecquAHb0uDJJRz/+RfR0AiG0XG0NW+C"
|
||||
}
|
||||
@@ -1159,6 +1159,26 @@ const tables = {
|
||||
selectbox_value: 'FORMULA',
|
||||
selectbox_order: 1,
|
||||
ver_to_dttm: 127490111999
|
||||
},
|
||||
{
|
||||
selectbox_rk: 73,
|
||||
ver_from_dttm: 0,
|
||||
select_lib: dcLibref,
|
||||
select_ds: 'MPE_DERIVED',
|
||||
base_column: 'VAR_TYPE',
|
||||
selectbox_value: 'C',
|
||||
selectbox_order: 1,
|
||||
ver_to_dttm: 127490111999
|
||||
},
|
||||
{
|
||||
selectbox_rk: 74,
|
||||
ver_from_dttm: 0,
|
||||
select_lib: dcLibref,
|
||||
select_ds: 'MPE_DERIVED',
|
||||
base_column: 'VAR_TYPE',
|
||||
selectbox_value: 'N',
|
||||
selectbox_order: 2,
|
||||
ver_to_dttm: 127490111999
|
||||
}
|
||||
],
|
||||
MPE_SUBMIT: [],
|
||||
@@ -1371,6 +1391,19 @@ const tables = {
|
||||
'RLS_SCOPE RLS_GROUP RLS_LIBREF RLS_TABLE RLS_GROUP_LOGIC RLS_SUBGROUP_LOGIC RLS_SUBGROUP_ID RLS_VARIABLE_NM RLS_OPERATOR_NM RLS_RAW_VALUE',
|
||||
post_edit_hook: 'services/hooks/mpe_row_level_security_postedit'
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
tx_to: 127490111999,
|
||||
libref: dcLibref,
|
||||
dsn: 'MPE_DERIVED',
|
||||
num_of_approvals_required: 1,
|
||||
loadtype: 'TXTEMPORAL',
|
||||
buskey: 'LIBREF DSN VAR_NM',
|
||||
notes: 'Derived (computed) variables for the EDIT screen',
|
||||
var_txfrom: 'TX_FROM',
|
||||
var_txto: 'TX_TO',
|
||||
post_edit_hook: 'services/hooks/mpe_derived_postedit'
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
tx_to: 127490111999,
|
||||
@@ -2002,6 +2035,206 @@ const tables = {
|
||||
rule_value: 'services/validations/mpe_x_test.some_num',
|
||||
rule_active: 1,
|
||||
tx_to: 127490111999
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
base_lib: dcLibref,
|
||||
base_ds: 'MPE_DERIVED',
|
||||
base_col: 'LIBREF',
|
||||
rule_type: 'NOTNULL',
|
||||
rule_value: ' ',
|
||||
rule_active: 1,
|
||||
tx_to: 127490111999
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
base_lib: dcLibref,
|
||||
base_ds: 'MPE_DERIVED',
|
||||
base_col: 'LIBREF',
|
||||
rule_type: 'CASE',
|
||||
rule_value: 'UPCASE',
|
||||
rule_active: 1,
|
||||
tx_to: 127490111999
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
base_lib: dcLibref,
|
||||
base_ds: 'MPE_DERIVED',
|
||||
base_col: 'DSN',
|
||||
rule_type: 'NOTNULL',
|
||||
rule_value: ' ',
|
||||
rule_active: 1,
|
||||
tx_to: 127490111999
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
base_lib: dcLibref,
|
||||
base_ds: 'MPE_DERIVED',
|
||||
base_col: 'DSN',
|
||||
rule_type: 'CASE',
|
||||
rule_value: 'UPCASE',
|
||||
rule_active: 1,
|
||||
tx_to: 127490111999
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
base_lib: dcLibref,
|
||||
base_ds: 'MPE_DERIVED',
|
||||
base_col: 'VAR_NM',
|
||||
rule_type: 'NOTNULL',
|
||||
rule_value: ' ',
|
||||
rule_active: 1,
|
||||
tx_to: 127490111999
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
base_lib: dcLibref,
|
||||
base_ds: 'MPE_DERIVED',
|
||||
base_col: 'VAR_NM',
|
||||
rule_type: 'CASE',
|
||||
rule_value: 'UPCASE',
|
||||
rule_active: 1,
|
||||
tx_to: 127490111999
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
base_lib: dcLibref,
|
||||
base_ds: 'MPE_DERIVED',
|
||||
base_col: 'VAR_NM',
|
||||
rule_type: 'HARDREGEX',
|
||||
rule_value: '^[A-Za-z_][A-Za-z0-9_]*$',
|
||||
rule_active: 1,
|
||||
tx_to: 127490111999
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
base_lib: dcLibref,
|
||||
base_ds: 'MPE_DERIVED',
|
||||
base_col: 'VAR_DEPS',
|
||||
rule_type: 'CASE',
|
||||
rule_value: 'UPCASE',
|
||||
rule_active: 1,
|
||||
tx_to: 127490111999
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
base_lib: dcLibref,
|
||||
base_ds: 'MPE_DERIVED',
|
||||
base_col: 'VAR_DEPS',
|
||||
rule_type: 'HARDREGEX',
|
||||
rule_value: '^[A-Za-z0-9_ ]*$',
|
||||
rule_active: 1,
|
||||
tx_to: 127490111999
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
base_lib: dcLibref,
|
||||
base_ds: 'MPE_DERIVED',
|
||||
base_col: 'VAR_TYPE',
|
||||
rule_type: 'NOTNULL',
|
||||
rule_value: ' ',
|
||||
rule_active: 1,
|
||||
tx_to: 127490111999
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
base_lib: dcLibref,
|
||||
base_ds: 'MPE_DERIVED',
|
||||
base_col: 'VAR_TYPE',
|
||||
rule_type: 'CASE',
|
||||
rule_value: 'UPCASE',
|
||||
rule_active: 1,
|
||||
tx_to: 127490111999
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
base_lib: dcLibref,
|
||||
base_ds: 'MPE_DERIVED',
|
||||
base_col: 'VAR_TYPE',
|
||||
rule_type: 'HARDSELECT',
|
||||
rule_value: ' ',
|
||||
rule_active: 1,
|
||||
tx_to: 127490111999
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
base_lib: dcLibref,
|
||||
base_ds: 'MPE_DERIVED',
|
||||
base_col: 'VAR_LEN',
|
||||
rule_type: 'NOTNULL',
|
||||
rule_value: ' ',
|
||||
rule_active: 1,
|
||||
tx_to: 127490111999
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
base_lib: dcLibref,
|
||||
base_ds: 'MPE_DERIVED',
|
||||
base_col: 'VAR_LEN',
|
||||
rule_type: 'MINVAL',
|
||||
rule_value: '1',
|
||||
rule_active: 1,
|
||||
tx_to: 127490111999
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
base_lib: dcLibref,
|
||||
base_ds: 'MPE_DERIVED',
|
||||
base_col: 'VAR_LEN',
|
||||
rule_type: 'MAXVAL',
|
||||
rule_value: '32767',
|
||||
rule_active: 1,
|
||||
tx_to: 127490111999
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
base_lib: dcLibref,
|
||||
base_ds: 'MPE_DERIVED',
|
||||
base_col: 'VAR_FMT',
|
||||
rule_type: 'CASE',
|
||||
rule_value: 'UPCASE',
|
||||
rule_active: 1,
|
||||
tx_to: 127490111999
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
base_lib: dcLibref,
|
||||
base_ds: 'MPE_DERIVED',
|
||||
base_col: 'VAR_FMT',
|
||||
rule_type: 'HARDREGEX',
|
||||
rule_value: '^(\$?[A-Za-z][A-Za-z0-9_]*(\.[0-9]+)?\.?|\$?[0-9]+(\.[0-9]+)?\.?)?$',
|
||||
rule_active: 1,
|
||||
tx_to: 127490111999
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
base_lib: dcLibref,
|
||||
base_ds: 'MPE_DERIVED',
|
||||
base_col: 'VAR_LOGIC',
|
||||
rule_type: 'NOTNULL',
|
||||
rule_value: ' ',
|
||||
rule_active: 1,
|
||||
tx_to: 127490111999
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
base_lib: dcLibref,
|
||||
base_ds: 'MPE_DERIVED',
|
||||
base_col: 'VAR_LABEL',
|
||||
rule_type: 'HARDREGEX',
|
||||
rule_value: '^[^\\x00-\\x1F\\x7F]*$',
|
||||
rule_active: 1,
|
||||
tx_to: 127490111999
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
base_lib: dcLibref,
|
||||
base_ds: 'MPE_DERIVED',
|
||||
base_col: 'VAR_LOGIC',
|
||||
rule_type: 'HARDREGEX',
|
||||
rule_value: '^(?!.*(/\\*|\\*/))[^;&%]*$',
|
||||
rule_active: 1,
|
||||
tx_to: 127490111999
|
||||
}
|
||||
],
|
||||
MPE_X_TEST: [
|
||||
|
||||
@@ -47,7 +47,8 @@ if (!tableId || !libds) {
|
||||
webOutClose()
|
||||
} else {
|
||||
// The SAS service also checks EDIT or APPROVE access (mpe_accesscheck). The
|
||||
// mock has no permission store, so access is not emulated.
|
||||
// mock seeds MPE_SECURITY but no mock service enforces it, so access is not
|
||||
// emulated here.
|
||||
|
||||
// The approval folder is zipped as it stands. getauditfile.sas lists it with
|
||||
// mp_dirlist and adds each file to the package under contents/.
|
||||
|
||||
@@ -641,6 +641,27 @@ function zipUtf8(value) {
|
||||
return Uint8Array.from(out)
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders rows as CSV, the counterpart of %mp_ds2csv: the header row, then one
|
||||
* line per row, with values quoted only when they carry a delimiter, quote or
|
||||
* line break.
|
||||
*
|
||||
* @param {string[]} headers column names, in order
|
||||
* @param {Array<Object>} rows objects keyed by column name
|
||||
* @returns {string} CSV text with a trailing newline
|
||||
*/
|
||||
function toCsv(headers, rows) {
|
||||
const cell = (value) => {
|
||||
const text = value === undefined || value === null ? '' : String(value)
|
||||
return /[",\n\r]/.test(text) ? '"' + text.replace(/"/g, '""') + '"' : text
|
||||
}
|
||||
const lines = [headers.map(cell).join(',')]
|
||||
for (const row of rows) {
|
||||
lines.push(headers.map((header) => cell(row[header])).join(','))
|
||||
}
|
||||
return lines.join('\n') + '\n'
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a zip archive in memory, entries stored rather than deflated - the JS
|
||||
* counterpart of the ODS package getauditfile.sas publishes. The zip format
|
||||
@@ -661,16 +682,19 @@ function mpZipPackage(entries) {
|
||||
const name = zipUtf8(entry.name)
|
||||
const data = zipUtf8(entry.content)
|
||||
const crc = zipCrc32(data)
|
||||
// Bit 11 marks the name as UTF-8. Without it an extractor decodes a
|
||||
// non-ASCII name as CP437, so the entry cannot be read back by name.
|
||||
const flags = name.some((byte) => byte >= 0x80) ? 0x0800 : 0
|
||||
|
||||
// 1980-01-01 00:00, the earliest DOS timestamp: a stable value keeps
|
||||
// the archive byte-identical for identical inputs.
|
||||
const header = [].concat(
|
||||
u32(0x04034b50), u16(20), u16(0), u16(0), u16(0), u16(0x21),
|
||||
u32(0x04034b50), u16(20), u16(flags), u16(0), u16(0), u16(0x21),
|
||||
u32(crc), u32(data.length), u32(data.length), u16(name.length), u16(0)
|
||||
)
|
||||
|
||||
local.push(Uint8Array.from(header), name, data)
|
||||
central.push({ name, crc, size: data.length, offset })
|
||||
central.push({ name, crc, size: data.length, offset, flags })
|
||||
offset += header.length + name.length + data.length
|
||||
}
|
||||
|
||||
@@ -678,7 +702,7 @@ function mpZipPackage(entries) {
|
||||
let directorySize = 0
|
||||
for (const entry of central) {
|
||||
const header = [].concat(
|
||||
u32(0x02014b50), u16(20), u16(20), u16(0), u16(0), u16(0), u16(0x21),
|
||||
u32(0x02014b50), u16(20), u16(20), u16(entry.flags), u16(0), u16(0), u16(0x21),
|
||||
u32(entry.crc), u32(entry.size), u32(entry.size),
|
||||
u16(entry.name.length), u16(0), u16(0), u16(0), u16(0), u32(0),
|
||||
u32(entry.offset)
|
||||
|
||||
@@ -101,7 +101,7 @@ function pkFirst(columns, keys) {
|
||||
// ─── Derived (computed) variables (mpe_derived.sas) ─────────────────────────
|
||||
// Applied before the filter, so a filter can reference them. The derived
|
||||
// columns are added to the schema too, which is what puts them in COLS and in
|
||||
// the grid (read only, since the mock marks every column READ).
|
||||
// the grid; they are held read-only by a READONLY rule below.
|
||||
|
||||
const derived = mpeDerived({
|
||||
libref: requestedLibref,
|
||||
@@ -160,6 +160,16 @@ if (tableData && tableData.columns) {
|
||||
}
|
||||
}
|
||||
|
||||
// A derived variable is computed at read time, so the grid must not accept a
|
||||
// value for it. COLS carries READ for the same reason (a derived column has no
|
||||
// CLS rule row), but the client only applies COLS.CLS_RULE when CLS_FLAG is
|
||||
// set, which column-level security drives - so the rule is declared here too,
|
||||
// as getdata.sas does. dqrules is the request-local array the filter/map above
|
||||
// built, so appending to it leaves the loaded MPE_VALIDATIONS rows untouched.
|
||||
for (const v of derived) {
|
||||
dqrules.push({ BASE_COL: v.name, RULE_TYPE: 'READONLY', RULE_VALUE: '' })
|
||||
}
|
||||
|
||||
// ─── DQ dropdown data (from MPE_SELECTBOX + SOFTSELECT from table data) ──────
|
||||
// MPE_SELECTBOX lives in the control library.
|
||||
|
||||
|
||||
@@ -59,6 +59,9 @@ for (let i = 1; i <= 10; i++) {
|
||||
|
||||
let libds = ''
|
||||
if (typeof table !== 'undefined' && table) libds = String(table).toUpperCase()
|
||||
// captured before any hook can re-point the submission: the cleaned CSV is
|
||||
// named after the original table, as loadfile.sas names it
|
||||
const origLibds = libds
|
||||
let libref = libds.split('.')[0] || dcLibref
|
||||
let dsn = libds.split('.')[1] || ''
|
||||
|
||||
@@ -178,6 +181,9 @@ if (!msg) {
|
||||
// written, while LIBREF / DS are still ordinary macro variables, so it
|
||||
// can re-point the changeset at a different table. The mock eval()s the
|
||||
// hook's .js counterpart in this scope so it can reassign libref / dsn.
|
||||
// A validating hook rejects the submission by setting hookErrmsg (see
|
||||
// hooks/mpe_derived_postedit.js), which stops the staging below.
|
||||
let hookErrmsg = ''
|
||||
const hookTables = mpeLoadTableData('MPE_TABLES')
|
||||
const hookReg = (hookTables && hookTables.rows)
|
||||
? hookTables.rows.find(r => r.libref === libref && r.dsn === dsn)
|
||||
@@ -185,16 +191,21 @@ if (!msg) {
|
||||
if (hookReg && hookReg.post_edit_hook) {
|
||||
const hookSrc = mockHookSource(hookReg.post_edit_hook)
|
||||
if (hookSrc) {
|
||||
// a hook reads and normalises the submitted rows as jsDataRows
|
||||
// (stagedata.js names them that); here they are `rows`
|
||||
const jsDataRows = rows
|
||||
try {
|
||||
eval(hookSrc)
|
||||
} catch (err) {
|
||||
console.log(
|
||||
// a hook that throws has not validated anything, so the
|
||||
// submission is refused rather than staged unchecked
|
||||
hookErrmsg =
|
||||
'post_edit_hook ' + hookReg.post_edit_hook + ' failed: ' + err.message
|
||||
)
|
||||
}
|
||||
libds = libref + '.' + dsn
|
||||
}
|
||||
}
|
||||
if (hookErrmsg) msg = hookErrmsg
|
||||
|
||||
tableId = makeTableId()
|
||||
// ─── RK assignment (mpe_loader) ──────────────────────────────────────
|
||||
@@ -214,7 +225,7 @@ if (!msg) {
|
||||
if (rkName && !headers.some((h) => h.toUpperCase() === rkName.toUpperCase())) {
|
||||
headers.push(rkName)
|
||||
}
|
||||
stageSubmission({
|
||||
if (!msg) stageSubmission({
|
||||
dataDir: libDataDir(libref),
|
||||
stagingDir,
|
||||
tableId,
|
||||
@@ -224,13 +235,11 @@ if (!msg) {
|
||||
message: 'File upload: ' + csvName,
|
||||
headers,
|
||||
rows,
|
||||
// loadfile.sas leaves two files in the approval folder: the cleaned
|
||||
// CSV (%mp_cleancsv) and a copy of the upload (BKP_<name>). The mock
|
||||
// only accepts CSV, so both hold the uploaded text.
|
||||
auditFiles: [
|
||||
{ name: libds + '.csv', content: csvText },
|
||||
{ name: 'BKP_' + csvName, content: csvText }
|
||||
]
|
||||
// loadfile.sas leaves the cleaned CSV (%mp_cleancsv) in the approval
|
||||
// folder, named after the table as the request found it. It copies
|
||||
// the upload as BKP_<name> only when a second, non-CSV fileref came
|
||||
// with the request, which the mock does not accept - so no BKP here.
|
||||
auditFiles: [{ name: origLibds + '.csv', content: csvText }]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ let message = _sctRow.MESSAGE || ''
|
||||
let libds = _sctRow.LIBDS || ''
|
||||
let libref = libds.split('.')[0] || dcLibref
|
||||
let dsn = libds.split('.')[1] || ''
|
||||
// stagedata.sas names the CSV it stages after the table as the request found
|
||||
// it, before a hook can re-point the submission
|
||||
const origLibds = libds
|
||||
|
||||
// The base table lives in its own library (DC_JSLIB for control tables,
|
||||
// TESTDATA for demo user tables); the staging dir stays in the control lib.
|
||||
@@ -63,6 +66,10 @@ if (schema.length > 0) {
|
||||
// eval()s the hook's .js counterpart in this scope so it can reassign
|
||||
// libref / dsn / libds; the submit record then names the new base table, exactly
|
||||
// as the SAS hook does via call symputx.
|
||||
// A validating hook rejects the submission by setting hookErrmsg (see
|
||||
// hooks/mpe_derived_postedit.js); the response below turns it into an abort,
|
||||
// and nothing is staged.
|
||||
let hookErrmsg = ''
|
||||
const hookTables = makeTableLoader(mpeDataDir)('MPE_TABLES')
|
||||
const hookReg = (hookTables && hookTables.rows)
|
||||
? hookTables.rows.find(r => r.libref === libref && r.dsn === dsn)
|
||||
@@ -73,9 +80,10 @@ if (hookReg && hookReg.post_edit_hook) {
|
||||
try {
|
||||
eval(hookSrc)
|
||||
} catch (err) {
|
||||
console.log(
|
||||
// a hook that throws has not validated anything, so the submission
|
||||
// is refused rather than staged unchecked
|
||||
hookErrmsg =
|
||||
'post_edit_hook ' + hookReg.post_edit_hook + ' failed: ' + err.message
|
||||
)
|
||||
}
|
||||
libds = libref + '.' + dsn
|
||||
}
|
||||
@@ -101,6 +109,29 @@ if (rkName && !jsDataHeaders.some((h) => h.toUpperCase() === rkName.toUpperCase(
|
||||
jsDataHeaders.push(rkName)
|
||||
}
|
||||
|
||||
// ─── Derived variables ───────────────────────────────────────────────────────
|
||||
// stagedata.sas drops the MPE_DERIVED droplist from the submitted data before
|
||||
// anything is staged (they are display-only), and the approval CSV is written
|
||||
// from that stripped dataset - so neither the staged rows nor the pack may
|
||||
// carry them.
|
||||
|
||||
const derivedVars = mpeDerived({
|
||||
libref,
|
||||
table: dsn,
|
||||
dataDir: mpeDataDir,
|
||||
columns: schema
|
||||
})
|
||||
if (derivedVars.length > 0) {
|
||||
const derivedNames = new Set(derivedVars.map((d) => d.name.toUpperCase()))
|
||||
const keep = (key) => !derivedNames.has(key.toUpperCase())
|
||||
jsDataHeaders = jsDataHeaders.filter(keep)
|
||||
jsDataRows = jsDataRows.map((row) => {
|
||||
const copy = {}
|
||||
for (const key of Object.keys(row)) if (keep(key)) copy[key] = row[key]
|
||||
return copy
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Stage the data ──────────────────────────────────────────────────────────
|
||||
// One subfolder per submission (DSID) under the staging directory, so that the
|
||||
// staged data can sit alongside related artifacts (submit/approval logs,
|
||||
@@ -108,7 +139,7 @@ if (rkName && !jsDataHeaders.some((h) => h.toUpperCase() === rkName.toUpperCase(
|
||||
|
||||
const tableId = makeTableId()
|
||||
|
||||
stageSubmission({
|
||||
if (!hookErrmsg) stageSubmission({
|
||||
dataDir,
|
||||
stagingDir,
|
||||
tableId,
|
||||
@@ -117,9 +148,17 @@ stageSubmission({
|
||||
dsn,
|
||||
message,
|
||||
headers: jsDataHeaders,
|
||||
rows: jsDataRows
|
||||
rows: jsDataRows,
|
||||
// stagedata.sas writes the staged rows as <orig_libds>.csv alongside them
|
||||
// (%mp_ds2csv into the approval folder), so the audit pack carries it
|
||||
auditFiles: [{ name: origLibds + '.csv', content: toCsv(jsDataHeaders, jsDataRows) }]
|
||||
})
|
||||
|
||||
webOutOpen()
|
||||
if (hookErrmsg) {
|
||||
webOutObj([{ MSG: hookErrmsg, MAC: 'stagedata' }], 'sasjsAbort')
|
||||
webOutObj([{ STATUS: 'ERROR', DSID: '' }], 'sasparams')
|
||||
} else {
|
||||
webOutObj([{ STATUS: 'SUCCESS', DSID: tableId, URL: '' }], 'sasparams')
|
||||
}
|
||||
webOutClose()
|
||||
@@ -0,0 +1,369 @@
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Mock of services/hooks/mpe_derived_postedit
|
||||
//
|
||||
// Registered against DC_JSLIB.MPE_DERIVED (MPE_TABLES.post_edit_hook). This
|
||||
// file is eval'd by editors/stagedata.js and editors/loadfile.js, mirroring how
|
||||
// mpe_loader.sas %includes the hook program via %mpe_runhook, so it shares the
|
||||
// caller's scope: jsDataRows (the submitted rows - work.staging_ds on the
|
||||
// server), libref, dsn, mpeDataDir and the dcMockUtils helpers.
|
||||
//
|
||||
// A hook rejects a submission by setting hookErrmsg, which the caller turns
|
||||
// into a sasjsAbort - the same outcome as the SAS hook's %mp_abort.
|
||||
//
|
||||
// The rules mirror services/hooks/mpe_derived_postedit.sas, in its order:
|
||||
// normalise the incoming rows, reject a duplicate name and a bad field, then
|
||||
// check the dependency graph that the resulting configuration would have (the
|
||||
// incoming rows plus the current rows they do not replace). As on the server,
|
||||
// the collision between VAR_NM and a physical column is left to the read-time
|
||||
// engine, which runs with the target library assigned - the hook does not have
|
||||
// it here either.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
;(function mpeDerivedPostedit() {
|
||||
const DELETE_MARK = '_____DELETE__THIS__RECORD_____'
|
||||
const NAME_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'
|
||||
const FMT_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$.'
|
||||
|
||||
const text = (v) => (v === undefined || v === null ? '' : String(v))
|
||||
const up = (v) => text(v).trim().toUpperCase()
|
||||
const strip = (v) => text(v).trim()
|
||||
|
||||
/** Reads a field as the hook normalised it. */
|
||||
const field = (row, name) => up(colVal(row, name))
|
||||
|
||||
/** Writes a field back under the key the row already carries. */
|
||||
const setField = (row, name, value) => {
|
||||
const key = Object.keys(row).find((k) => k.toUpperCase() === name)
|
||||
if (key) row[key] = value
|
||||
else row[name] = value
|
||||
}
|
||||
|
||||
const rows = Array.isArray(jsDataRows) ? jsDataRows : []
|
||||
const isDeleted = (row) => field(row, DELETE_MARK) === 'YES'
|
||||
const isLive = (row) =>
|
||||
!isDeleted(row) && Number(colVal(row, 'ACTIVE')) === 1
|
||||
const keyOf = (row) =>
|
||||
[field(row, 'LIBREF'), field(row, 'DSN'), field(row, 'VAR_NM')].join(
|
||||
'.'
|
||||
)
|
||||
|
||||
const reject = (msg) => {
|
||||
hookErrmsg = msg
|
||||
}
|
||||
|
||||
// ─── Normalise the incoming values ─────────────────────────────────────────
|
||||
for (const row of rows) {
|
||||
if (isDeleted(row)) continue
|
||||
for (const name of [
|
||||
'LIBREF',
|
||||
'DSN',
|
||||
'VAR_NM',
|
||||
'VAR_TYPE',
|
||||
'VAR_FMT',
|
||||
'VAR_DEPS'
|
||||
]) {
|
||||
setField(row, name, up(colVal(row, name)))
|
||||
}
|
||||
for (const name of ['VAR_LABEL', 'VAR_LOGIC']) {
|
||||
setField(row, name, strip(colVal(row, name)))
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Duplicate names after normalisation ───────────────────────────────────
|
||||
// mpe_loader compares the raw values, so a case variant reaches this point
|
||||
const seen = new Set()
|
||||
for (const row of rows) {
|
||||
if (isDeleted(row)) continue
|
||||
const key = keyOf(row)
|
||||
if (seen.has(key)) {
|
||||
return reject(
|
||||
`MPE_DERIVED: ${field(row, 'VAR_NM')} is declared more than once for ${field(row, 'LIBREF')}.${field(row, 'DSN')}`
|
||||
)
|
||||
}
|
||||
seen.add(key)
|
||||
}
|
||||
|
||||
// ─── Field level checks, the rules the engine applies at read time ─────────
|
||||
// C0 controls and DEL: what a single-line label cannot carry
|
||||
const CONTROL_CHARS = /[\u0000-\u001F\u007F]/
|
||||
|
||||
// true when the value carries a character outside the allowed set
|
||||
const hasInvalidChar = (value, chars) =>
|
||||
[...value].some((c) => !chars.includes(c))
|
||||
|
||||
for (const row of rows) {
|
||||
if (isDeleted(row)) continue
|
||||
const libref = field(row, 'LIBREF')
|
||||
const dsn = field(row, 'DSN')
|
||||
const name = field(row, 'VAR_NM')
|
||||
const type = field(row, 'VAR_TYPE')
|
||||
const len = Number(colVal(row, 'VAR_LEN'))
|
||||
const fmt = field(row, 'VAR_FMT')
|
||||
const label = strip(colVal(row, 'VAR_LABEL'))
|
||||
const logic = strip(colVal(row, 'VAR_LOGIC'))
|
||||
|
||||
let reason = ''
|
||||
if (!libref || !dsn) reason = 'LIBREF and DSN are required'
|
||||
else if (!name) reason = 'VAR_NM is required'
|
||||
else if (hasInvalidChar(name, NAME_CHARS))
|
||||
reason = 'VAR_NM must be a valid SAS name'
|
||||
else if ('0123456789'.includes(name[0]))
|
||||
reason = 'VAR_NM must not start with a digit'
|
||||
else if (name === DELETE_MARK) reason = 'VAR_NM is reserved'
|
||||
else if (type !== 'C' && type !== 'N')
|
||||
reason = 'VAR_TYPE must be C or N'
|
||||
else if (!Number.isInteger(len) || len < 1)
|
||||
reason = 'VAR_LEN must be a positive integer'
|
||||
else if (type === 'C' && len > 32767)
|
||||
reason = 'VAR_LEN exceeds 32767 for a character variable'
|
||||
else if (type === 'N' && len > 8)
|
||||
reason = 'VAR_LEN exceeds 8 for a numeric variable'
|
||||
else if (
|
||||
fmt.includes(';') ||
|
||||
fmt.includes('&') ||
|
||||
fmt.includes('%') ||
|
||||
fmt.includes('/*') ||
|
||||
fmt.includes('*/')
|
||||
)
|
||||
reason = 'VAR_FMT must be a format name, without ; & % or /* */'
|
||||
else if (hasInvalidChar(fmt, FMT_CHARS))
|
||||
reason = 'VAR_FMT must be a format name'
|
||||
// free text, in any script - what is refused is anything that cannot be
|
||||
// a single-line column label, and a value the engine would cut to 256
|
||||
else if (CONTROL_CHARS.test(label))
|
||||
reason =
|
||||
'VAR_LABEL must be plain text - no line breaks or control characters'
|
||||
else if ([...label].length > 256)
|
||||
reason = 'VAR_LABEL exceeds 256 characters'
|
||||
else if (!logic) reason = 'VAR_LOGIC is required'
|
||||
else if (logic.includes(';'))
|
||||
reason = 'VAR_LOGIC must be a single expression - remove the ;'
|
||||
else if (logic.includes('&') || logic.includes('%'))
|
||||
reason = 'VAR_LOGIC must not contain macro triggers'
|
||||
else if (logic.includes('/*') || logic.includes('*/'))
|
||||
reason = 'VAR_LOGIC must not contain comments'
|
||||
|
||||
if (reason) return reject(`MPE_DERIVED: ${name}: ${reason}`)
|
||||
}
|
||||
|
||||
// ─── The graph of the resulting configuration ──────────────────────────────
|
||||
// The incoming live rows plus the current rows they do not replace. Only
|
||||
// active rows are in it, because the engine reads nothing else - an inactive
|
||||
// row can neither resolve a dependency nor be resolved by one.
|
||||
const controlDir = mpeDataDir
|
||||
const loadControl = makeTableLoader(controlDir)
|
||||
const current = loadControl('MPE_DERIVED')
|
||||
const nowSeconds = Date.now() / 1000
|
||||
|
||||
// mpeDerived reads a row as current when TX_TO is missing, zero or in the
|
||||
// future (dcMockUtils.js, the read-time engine), so the same test is used
|
||||
// here - a seeded row with no TX_TO is live for the engine, and a dependency
|
||||
// on it has to validate
|
||||
const isCurrent = (row) =>
|
||||
!Number(colVal(row, 'TX_TO')) || Number(colVal(row, 'TX_TO')) > nowSeconds
|
||||
|
||||
const stagedKeys = new Set(rows.map(keyOf))
|
||||
const stagedLive = rows.filter(isLive)
|
||||
|
||||
// the control-table arm of the SAS hook is limited to the tables this
|
||||
// submission touches, so another table's configuration cannot block it
|
||||
const stagedTables = new Set(
|
||||
rows.map((row) => [field(row, 'LIBREF'), field(row, 'DSN')].join('.'))
|
||||
)
|
||||
|
||||
/** libref.dsn.name - the key every graph lookup uses. */
|
||||
const nodeKey = (n) => [n.libref, n.dsn, n.name].join('.')
|
||||
|
||||
const graph = new Map()
|
||||
for (const row of stagedLive) {
|
||||
graph.set(keyOf(row), {
|
||||
libref: field(row, 'LIBREF'),
|
||||
dsn: field(row, 'DSN'),
|
||||
name: field(row, 'VAR_NM'),
|
||||
deps: field(row, 'VAR_DEPS').split(/\s+/).filter(Boolean),
|
||||
logic: strip(colVal(row, 'VAR_LOGIC')).toUpperCase()
|
||||
})
|
||||
}
|
||||
const currentRows =
|
||||
current && Array.isArray(current.rows)
|
||||
? current.rows.filter(
|
||||
(r) =>
|
||||
isCurrent(r) &&
|
||||
Number(colVal(r, 'ACTIVE')) === 1 &&
|
||||
stagedTables.has(
|
||||
[up(colVal(r, 'LIBREF')), up(colVal(r, 'DSN'))].join(
|
||||
'.'
|
||||
)
|
||||
) &&
|
||||
!stagedKeys.has(
|
||||
[
|
||||
up(colVal(r, 'LIBREF')),
|
||||
up(colVal(r, 'DSN')),
|
||||
up(colVal(r, 'VAR_NM'))
|
||||
].join('.')
|
||||
)
|
||||
)
|
||||
: []
|
||||
for (const row of currentRows) {
|
||||
graph.set(
|
||||
[
|
||||
up(colVal(row, 'LIBREF')),
|
||||
up(colVal(row, 'DSN')),
|
||||
up(colVal(row, 'VAR_NM'))
|
||||
].join('.'),
|
||||
{
|
||||
libref: up(colVal(row, 'LIBREF')),
|
||||
dsn: up(colVal(row, 'DSN')),
|
||||
name: up(colVal(row, 'VAR_NM')),
|
||||
deps: up(colVal(row, 'VAR_DEPS')).split(/\s+/).filter(Boolean),
|
||||
logic: strip(colVal(row, 'VAR_LOGIC')).toUpperCase()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** Names this load leaves inactive, which therefore cannot resolve a dep. */
|
||||
const inactive = new Set()
|
||||
const liveKeys = new Set(stagedLive.map(keyOf))
|
||||
for (const row of rows) {
|
||||
const key = keyOf(row)
|
||||
if (!liveKeys.has(key)) inactive.add(key)
|
||||
}
|
||||
if (current && Array.isArray(current.rows)) {
|
||||
for (const row of current.rows) {
|
||||
const key = [
|
||||
up(colVal(row, 'LIBREF')),
|
||||
up(colVal(row, 'DSN')),
|
||||
up(colVal(row, 'VAR_NM'))
|
||||
].join('.')
|
||||
if (
|
||||
isCurrent(row) &&
|
||||
Number(colVal(row, 'ACTIVE')) !== 1 &&
|
||||
!liveKeys.has(key)
|
||||
) {
|
||||
inactive.add(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of graph.values()) {
|
||||
for (const dep of node.deps) {
|
||||
const key = [node.libref, node.dsn, dep].join('.')
|
||||
if (inactive.has(key) && !graph.has(key)) {
|
||||
return reject(
|
||||
`MPE_DERIVED: ${node.name}: VAR_DEPS lists ${dep}, which is not active after this load - the engine cannot resolve it`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Self reference, undeclared and stale dependencies ─────────────────────
|
||||
/** The identifiers an expression uses, with quoted runs removed. */
|
||||
const identifiers = (logic) =>
|
||||
logic
|
||||
.replace(/'([^']|'')*'/g, ' ')
|
||||
.replace(/"([^"]|"")*"/g, ' ')
|
||||
.replace(/[^A-Za-z0-9_]/g, ' ')
|
||||
.toUpperCase()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
|
||||
// scoped per table, as the server does: a name that is a derived variable
|
||||
// on another table says nothing about this one
|
||||
const graphKeys = new Set([...graph.values()].map((n) => nodeKey(n)))
|
||||
|
||||
// one check at a time, in the order the server runs them, so the message a
|
||||
// row gets is the one it would get there
|
||||
for (const node of graph.values()) {
|
||||
if (node.deps.includes(node.name)) {
|
||||
return reject(
|
||||
`MPE_DERIVED: ${node.name}: VAR_DEPS may not list itself`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of graph.values()) {
|
||||
for (const token of identifiers(node.logic)) {
|
||||
if (
|
||||
graphKeys.has([node.libref, node.dsn, token].join('.')) &&
|
||||
token !== node.name &&
|
||||
!node.deps.includes(token)
|
||||
) {
|
||||
return reject(
|
||||
`MPE_DERIVED: ${node.name}: VAR_LOGIC references ${token}, which is not in VAR_DEPS`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of graph.values()) {
|
||||
const tokens = identifiers(node.logic)
|
||||
for (const dep of node.deps) {
|
||||
if (!tokens.includes(dep)) {
|
||||
return reject(
|
||||
`MPE_DERIVED: ${node.name}: VAR_DEPS lists ${dep}, which VAR_LOGIC does not reference`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── A derived variable may not be named by a row level security rule ──────
|
||||
const rls = loadControl('MPE_ROW_LEVEL_SECURITY')
|
||||
if (rls && Array.isArray(rls.rows)) {
|
||||
for (const node of graph.values()) {
|
||||
const match = rls.rows.find(
|
||||
(r) =>
|
||||
up(colVal(r, 'RLS_LIBREF')) === node.libref &&
|
||||
up(colVal(r, 'RLS_TABLE')) === node.dsn &&
|
||||
up(colVal(r, 'RLS_VARIABLE_NM')) === node.name &&
|
||||
Number(colVal(r, 'RLS_ACTIVE')) === 1 &&
|
||||
isCurrent(r)
|
||||
)
|
||||
if (match) {
|
||||
return reject(
|
||||
`MPE_DERIVED: ${node.name} is named by a row level security rule on ${node.libref}.${node.dsn} - derived variables cannot be used there`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Cycles ────────────────────────────────────────────────────────────────
|
||||
// Repeatedly place the variables whose derived inputs are already placed; a
|
||||
// round that places nothing leaves a cycle. The path is walked so the
|
||||
// message names it, as the SAS hook does.
|
||||
const nodeKeys = new Map(
|
||||
[...graph.values()].map((n) => [n, [n.libref, n.dsn, n.name].join('.')])
|
||||
)
|
||||
const placed = new Set()
|
||||
let remaining = [...graph.values()]
|
||||
|
||||
while (remaining.length > 0) {
|
||||
const ready = remaining.filter((n) =>
|
||||
n.deps.every((d) => {
|
||||
const key = [n.libref, n.dsn, d].join('.')
|
||||
return !graph.has(key) || placed.has(key)
|
||||
})
|
||||
)
|
||||
if (ready.length === 0) {
|
||||
// walk the unplaced edges so the message names the cycle
|
||||
const start = remaining[0]
|
||||
const path = [start.name]
|
||||
const visited = new Set([nodeKeys.get(start)])
|
||||
let cursor = start
|
||||
for (let i = 0; i < remaining.length; i++) {
|
||||
const parentKey = cursor.deps
|
||||
.map((d) => [cursor.libref, cursor.dsn, d].join('.'))
|
||||
.find((key) => graph.has(key) && !placed.has(key))
|
||||
if (!parentKey) break
|
||||
path.push(graph.get(parentKey).name)
|
||||
if (visited.has(parentKey)) break
|
||||
visited.add(parentKey)
|
||||
cursor = graph.get(parentKey)
|
||||
}
|
||||
return reject(
|
||||
`MPE_DERIVED: VAR_DEPS contains a cycle: ${path.join(' -> ')}`
|
||||
)
|
||||
}
|
||||
for (const node of ready) placed.add(nodeKeys.get(node))
|
||||
remaining = remaining.filter((n) => !placed.has(nodeKeys.get(n)))
|
||||
}
|
||||
})()
|
||||
@@ -49,6 +49,23 @@ if (!tableData && dsn) {
|
||||
if (found) tableData = found.data
|
||||
}
|
||||
|
||||
// ─── Derived variables (mpe_derived.sas) ─────────────────────────────────────
|
||||
// A stored filter can reference a derived column, so the derivation has to run
|
||||
// before the filter. The columns themselves are dropped from the output - they
|
||||
// belong to the EDIT screen, as viewdata.sas drops them.
|
||||
// a copy: the derivation below returns a new column list, and every use here
|
||||
// (cols, search, rows, vars) must keep looking at the physical columns only
|
||||
const physicalColumns = [...(tableData && tableData.columns ? tableData.columns : [])]
|
||||
const derived = mpeDerived({
|
||||
libref: libref,
|
||||
table: dsn,
|
||||
dataDir: mpeDataDir,
|
||||
columns: physicalColumns
|
||||
})
|
||||
if (tableData && derived.length > 0) {
|
||||
tableData = mpeDeriveRows(tableData, derived)
|
||||
}
|
||||
|
||||
// ─── Apply the stored filter (mpe_filtermaster, VIEW mode) ───────────────────
|
||||
// Mirrors viewdata.sas: %mpe_filtermaster(VIEW,&libds,filter_rk=&filter_rk)
|
||||
// Row Level Security and any saved filter are applied here, before the search.
|
||||
@@ -103,7 +120,7 @@ let nobs = 0
|
||||
if (tableData && tableData.columns) {
|
||||
// mpGetcols, the JS counterpart of %mp_getcols.sas - the macro viewdata.sas
|
||||
// calls for its own column list.
|
||||
cols = mpGetcols(tableData.columns).map(c => {
|
||||
cols = mpGetcols(physicalColumns).map(c => {
|
||||
const ddtype = c.DDTYPE === 'CHARACTER' ? 'C' : c.DDTYPE === 'NUMERIC' ? 'N' : c.DDTYPE
|
||||
const formatStr = c.TYPE === 'N' ? (c.FORMAT || '8.') : ('$' + c.LENGTH + '.')
|
||||
return {
|
||||
@@ -119,7 +136,10 @@ if (tableData && tableData.columns) {
|
||||
|
||||
let rows
|
||||
if (searchType) {
|
||||
const matches = filteredRows.filter(row => rowMatchesSearch(row, tableData.columns, searchType, searchVal))
|
||||
// the derived columns are dropped before the search, so only the
|
||||
// physical columns are searched (viewdata.sas drops them in the
|
||||
// vwsearch data step mp_searchdata then reads)
|
||||
const matches = filteredRows.filter(row => rowMatchesSearch(row, physicalColumns, searchType, searchVal))
|
||||
// mp_searchdata caps its output at outobs=&DC_MAXOBS_WEBVIEW, and
|
||||
// viewdata.sas then keeps only `if _n_<&DC_MAXOBS_WEBVIEW` of them.
|
||||
nobs = Math.min(matches.length, maxObsWebView)
|
||||
@@ -134,7 +154,7 @@ if (tableData && tableData.columns) {
|
||||
// viewdata rows: all values as strings (formatted, SAS proc-print style)
|
||||
viewdata = rows.map(r => {
|
||||
const row = {}
|
||||
for (const col of tableData.columns) {
|
||||
for (const col of physicalColumns) {
|
||||
const lcName = col.name.toLowerCase()
|
||||
let val = r[col.name]
|
||||
if (val === undefined) val = r[lcName]
|
||||
@@ -174,15 +194,15 @@ if (tableData && tableData.columns) {
|
||||
// (SAS missing), character columns get "".
|
||||
// A search that matches nothing sends no rows at all (the client shows
|
||||
// its "no data found" panel instead of an empty grid).
|
||||
if (viewdata.length === 0 && !searchType && tableData.columns.length > 0) {
|
||||
if (viewdata.length === 0 && !searchType && physicalColumns.length > 0) {
|
||||
const emptyRow = {}
|
||||
for (const col of tableData.columns) emptyRow[col.name] = col.type === 'N' ? '.' : ''
|
||||
for (const col of physicalColumns) emptyRow[col.name] = col.type === 'N' ? '.' : ''
|
||||
viewdata = [emptyRow]
|
||||
}
|
||||
|
||||
// $viewdata vars: keep original SAS types (num stays num, char stays char)
|
||||
vars = {}
|
||||
for (const col of tableData.columns) {
|
||||
for (const col of physicalColumns) {
|
||||
vars[col.name] = sasVarsEntry(col)
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -6,7 +6,7 @@
|
||||
"": {
|
||||
"name": "dc-sas",
|
||||
"dependencies": {
|
||||
"@sasjs/cli": "6.1.0",
|
||||
"@sasjs/cli": "6.2.0",
|
||||
"@sasjs/core": "6.0.0"
|
||||
}
|
||||
},
|
||||
@@ -165,9 +165,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/cli": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/cli/-/cli-6.1.0.tgz",
|
||||
"integrity": "sha512-i2CdGS69EM6Sn7XfR1ltBk9Bxz6gEV9xTGSWUmk4eVe+XnJBlYHyg93VgMcx7sWCtzRF9Dv7N2p7eXEc2DHfbQ==",
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/cli/-/cli-6.2.0.tgz",
|
||||
"integrity": "sha512-zovAOCmyPqcm+gooAZgwvgGH8n5cas3sY0lA80+hyFoFtmZ8VjW/bh19LM8OVwheT9dBCRCx4fnHwjy5XGpzvQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@sasjs/adapter": "^5.1.1",
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@sasjs/cli": "6.1.0",
|
||||
"@sasjs/cli": "6.2.0",
|
||||
"@sasjs/core": "6.0.0"
|
||||
},
|
||||
"overrides": {
|
||||
|
||||
@@ -162,8 +162,14 @@ insert into &dclib..MPE_VALIDATIONS set
|
||||
tx_from=0,base_lib="&dclib",base_ds="MPE_DERIVED",base_col="VAR_LOGIC"
|
||||
,rule_type='NOTNULL',rule_value=' ',rule_active=1
|
||||
,tx_to='31DEC5999:23:59:59'dt;
|
||||
insert into &dclib..MPE_VALIDATIONS set
|
||||
tx_from=0,base_lib="&dclib",base_ds="MPE_DERIVED",base_col="VAR_LABEL"
|
||||
,rule_type='HARDREGEX'
|
||||
,rule_value='^[^\x00-\x1F\x7F]*$',rule_active=1
|
||||
,tx_to='31DEC5999:23:59:59'dt;
|
||||
insert into &dclib..MPE_VALIDATIONS set
|
||||
tx_from=0,base_lib="&dclib",base_ds="MPE_DERIVED",base_col="VAR_LOGIC"
|
||||
,rule_type='HARDREGEX',rule_value='^[^;&%]*$',rule_active=1
|
||||
,rule_type='HARDREGEX'
|
||||
,rule_value='^(?!.*(/\*|\*/))[^;&%]*$',rule_active=1
|
||||
,tx_to='31DEC5999:23:59:59'dt;
|
||||
quit;
|
||||
@@ -2052,9 +2052,15 @@ insert into &lib..MPE_VALIDATIONS set
|
||||
tx_from=0,base_lib="&lib",base_ds="MPE_DERIVED",base_col="VAR_LOGIC"
|
||||
,rule_type='NOTNULL',rule_value=' ',rule_active=1
|
||||
,tx_to='31DEC5999:23:59:59'dt;
|
||||
insert into &lib..MPE_VALIDATIONS set
|
||||
tx_from=0,base_lib="&lib",base_ds="MPE_DERIVED",base_col="VAR_LABEL"
|
||||
,rule_type='HARDREGEX'
|
||||
,rule_value='^[^\x00-\x1F\x7F]*$',rule_active=1
|
||||
,tx_to='31DEC5999:23:59:59'dt;
|
||||
insert into &lib..MPE_VALIDATIONS set
|
||||
tx_from=0,base_lib="&lib",base_ds="MPE_DERIVED",base_col="VAR_LOGIC"
|
||||
,rule_type='HARDREGEX',rule_value='^[^;&%]*$',rule_active=1
|
||||
,rule_type='HARDREGEX'
|
||||
,rule_value='^(?!.*(/\*|\*/))[^;&%]*$',rule_active=1
|
||||
,tx_to='31DEC5999:23:59:59'dt;
|
||||
insert into &lib..MPE_VALIDATIONS set
|
||||
tx_from=0
|
||||
|
||||
@@ -340,8 +340,10 @@ select upcase(loadtype)
|
||||
);
|
||||
%put sourcevars=&sourcevars;
|
||||
/* the payload is capped at &maxrows - whichever limit that works out to be
|
||||
- this is what goes to the browser */
|
||||
options obs=&maxrows;
|
||||
- this is what goes to the browser. The cap is a data set option on the
|
||||
SET, not `options obs=`, so no later step in this session can be cut by it:
|
||||
a session option left set would truncate the metadata steps below, and a
|
||||
table wide enough to hit the cell limit would lose columns rather than rows. */
|
||||
data outdata;
|
||||
/* pk fields come first */
|
||||
%if %length(&pk)>0 %then %do;
|
||||
@@ -355,9 +357,8 @@ select upcase(loadtype)
|
||||
/* send empty row if empty table to help with hot rendering */
|
||||
output;
|
||||
%end;
|
||||
set work.out ;
|
||||
set work.out(obs=&maxrows) ;
|
||||
run;
|
||||
options obs=max;
|
||||
|
||||
|
||||
|
||||
@@ -661,7 +662,16 @@ proc sort data=&mpelib..mpe_validations
|
||||
by base_col rule_type rule_value;
|
||||
run;
|
||||
|
||||
/* merge with NOTNULL constraints in the physical table */
|
||||
/* merge with NOTNULL constraints in the physical table, and mark the derived
|
||||
variables read-only: a derived column is computed at read time, so the grid
|
||||
must not accept a value for it. COLS carries the same READ (a derived column
|
||||
has no CLS rule row) but the client only applies COLS.CLS_RULE when CLS_FLAG
|
||||
is set, which is driven by the table's column-level security - so the rule is
|
||||
declared here, where it holds regardless.
|
||||
|
||||
work.derived_vars exists on every path that reaches this step: %mpe_derived
|
||||
writes it before returning, empty when the table has no derived variables,
|
||||
so the union below needs no guard */
|
||||
proc sql;
|
||||
create table _data_ as
|
||||
select * from dqrules
|
||||
@@ -674,6 +684,11 @@ union
|
||||
and upcase(memname)="%scan(&orig_libds,2,.)"
|
||||
and upcase(name) in (select name from vars3)
|
||||
and notnull='yes'
|
||||
union
|
||||
select upcase(name) as base_col
|
||||
,'READONLY' as rule_type
|
||||
,'' as rule_value
|
||||
from work.derived_vars
|
||||
order by 1,2,3;
|
||||
data dqrules;
|
||||
set &syslast;
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
rows x 1004 columns, created and registered by testsetup) filtered to 200
|
||||
rows: that is under the row limit but 200 x 1004 = 200,800 cells, over the
|
||||
200,000 cell limit, so the cell limit decides the cap -
|
||||
floor(200000 / 1004) = 199 rows.
|
||||
floor(200000 / 1004) = 199 rows. It also checks the column list is whole:
|
||||
the cap applies to the rows the browser is sent, never to the COLS metadata.
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mp_assert.sas
|
||||
@@ -147,6 +148,27 @@ run;
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/**
|
||||
* The row cap must not reach the metadata: the COLS payload for this table
|
||||
* describes every column, not the 199 rows the cell limit admits. A row cap
|
||||
* left set as a session option would cut the column list to 199 entries, and
|
||||
* a grid losing 800 columns looks like a missing table rather than a limit.
|
||||
*/
|
||||
%let t2_cols=-1;
|
||||
%let t2_phys=-1;
|
||||
proc sql noprint;
|
||||
select count(*) into: t2_cols from web2.cols;
|
||||
select count(*) into: t2_phys
|
||||
from dictionary.columns
|
||||
where libname='DCTEST' and memname='WIDEBOY';
|
||||
quit;
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(&t2_cols=&t2_phys),
|
||||
desc=Test 2 - every column is described, not just the rows the cap admits,
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/**
|
||||
* dump results to the log for offline inspection
|
||||
*/
|
||||
|
||||
@@ -372,6 +372,19 @@ run;
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/* COLS.CLS_RULE only reaches the grid when CLS_FLAG is set, which a table
|
||||
with no column level security does not do - the read-only promise is
|
||||
carried by this rule instead */
|
||||
data work.chk4e;
|
||||
set web4.dqrules;
|
||||
where upcase(base_col)='DERIVED_NOTE' and rule_type='READONLY';
|
||||
run;
|
||||
%mp_assertdsobs(work.chk4e,
|
||||
desc=Test4 - derived column carries a READONLY rule,
|
||||
test=EQUALS 1,
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
data work.chk4b;
|
||||
set work.cols4;
|
||||
where name='DERIVED_NOTE' and label=:'DERIVED_NOTE=';
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
6. an inactive row is set active again while another active row depends on
|
||||
it - the re-activation is what makes the dependency resolvable
|
||||
|
||||
And on the label:
|
||||
8. a VAR_LABEL carrying a line break is rejected - a label is one line
|
||||
9. a label of free text (quotes, and the ; & % the other columns refuse) is
|
||||
accepted
|
||||
10. a label over 256 characters is rejected rather than silently cut down
|
||||
|
||||
The staged loads are aborted by the post edit hook inside the child service.
|
||||
An aborted load leaves its MPE_LOADS row IN PROGRESS, so each assertion
|
||||
matches on the unique reason_txt (the submitted message) rather than any load
|
||||
@@ -578,6 +584,184 @@ run;
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
|
||||
/**
|
||||
* Test 8 - a label carrying a line break must be rejected. VAR_LABEL is free
|
||||
* text and takes any character, including the quotes and triggers the other
|
||||
* columns refuse, but a column label is one line: the generated attrib
|
||||
* statement would carry the break into the DATA step that is included.
|
||||
*/
|
||||
%let t8=%sysfunc(datetime());
|
||||
|
||||
proc sql noprint;
|
||||
delete from &dc_libref..mpe_derived
|
||||
where upcase(libref)='DCTEST' and upcase(dsn)='DC_DERIV';
|
||||
quit;
|
||||
|
||||
data work.sascontroltable;
|
||||
action='LOAD';
|
||||
message='A label with a line break should abort';
|
||||
libds="&dc_libref..MPE_DERIVED";
|
||||
output;
|
||||
stop;
|
||||
run;
|
||||
|
||||
data work.jsdata;
|
||||
length LIBREF $8 DSN $32 VAR_NM $32 VAR_DEPS $4000 VAR_TYPE $1
|
||||
VAR_LOGIC $4000 VAR_FMT $32 VAR_LABEL $256
|
||||
_____DELETE__THIS__RECORD_____ $3;
|
||||
LIBREF='DCTEST';
|
||||
DSN='DC_DERIV';
|
||||
VAR_NM='LINEBREAK_LABEL';
|
||||
VAR_DEPS='';
|
||||
VAR_TYPE='N';
|
||||
VAR_LEN=8;
|
||||
VAR_FMT='';
|
||||
VAR_LOGIC='1';
|
||||
ACTIVE=1;
|
||||
VAR_LABEL=cats('first line','0A'x,'second line');
|
||||
_____DELETE__THIS__RECORD_____='No';
|
||||
run;
|
||||
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputdatasets=work.sascontroltable work.jsdata,
|
||||
outlib=web8,
|
||||
outref=wb8,
|
||||
mdebug=&sasjs_mdebug
|
||||
)
|
||||
|
||||
%let abort8=0;
|
||||
proc sql noprint;
|
||||
select count(*) into: abort8
|
||||
from &dc_libref..mpe_loads
|
||||
where processed_dttm>&t8
|
||||
and reason_txt='A label with a line break should abort'
|
||||
and upcase(status) ne 'SUCCESS';
|
||||
quit;
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(&abort8>0),
|
||||
desc=Checking a label with a line break is rejected,
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/**
|
||||
* Test 9 - a label made of anything else is accepted: quotes, and the
|
||||
* characters VAR_FMT and VAR_LOGIC refuse. The label is free text, and the
|
||||
* quotes are escaped when the attrib statement is built.
|
||||
*/
|
||||
proc sql noprint;
|
||||
delete from &dc_libref..mpe_derived
|
||||
where upcase(libref)='DCTEST' and upcase(dsn)='DC_DERIV';
|
||||
quit;
|
||||
|
||||
data work.sascontroltable;
|
||||
action='LOAD';
|
||||
message='Free text in a label should not abort';
|
||||
libds="&dc_libref..MPE_DERIVED";
|
||||
output;
|
||||
stop;
|
||||
run;
|
||||
|
||||
data work.jsdata;
|
||||
length LIBREF $8 DSN $32 VAR_NM $32 VAR_DEPS $4000 VAR_TYPE $1
|
||||
VAR_LOGIC $4000 VAR_FMT $32 VAR_LABEL $256
|
||||
_____DELETE__THIS__RECORD_____ $3;
|
||||
LIBREF='DCTEST';
|
||||
DSN='DC_DERIV';
|
||||
VAR_NM='FREE_TEXT_LABEL';
|
||||
VAR_DEPS='';
|
||||
VAR_TYPE='N';
|
||||
VAR_LEN=8;
|
||||
VAR_FMT='';
|
||||
VAR_LOGIC='1';
|
||||
ACTIVE=1;
|
||||
VAR_LABEL="it's 100% & more; really";
|
||||
_____DELETE__THIS__RECORD_____='No';
|
||||
run;
|
||||
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputdatasets=work.sascontroltable work.jsdata,
|
||||
outlib=web9,
|
||||
outref=wb9,
|
||||
mdebug=&sasjs_mdebug
|
||||
)
|
||||
|
||||
%let status9=0;
|
||||
data _null_;
|
||||
set web9.sasparams;
|
||||
putlog (_all_)(=);
|
||||
if status='SUCCESS' then call symputx('status9',1);
|
||||
run;
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(&status9=1),
|
||||
desc=Checking free text in a label is accepted,
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/**
|
||||
* Test 10 - a label longer than 256 is rejected rather than cut down: the
|
||||
* engine truncates the label to 256 when it builds the attrib statement, and
|
||||
* nothing reports it.
|
||||
*/
|
||||
%let t10=%sysfunc(datetime());
|
||||
|
||||
proc sql noprint;
|
||||
delete from &dc_libref..mpe_derived
|
||||
where upcase(libref)='DCTEST' and upcase(dsn)='DC_DERIV';
|
||||
quit;
|
||||
|
||||
data work.sascontroltable;
|
||||
action='LOAD';
|
||||
message='A label over 256 characters should abort';
|
||||
libds="&dc_libref..MPE_DERIVED";
|
||||
output;
|
||||
stop;
|
||||
run;
|
||||
|
||||
data work.jsdata;
|
||||
length LIBREF $8 DSN $32 VAR_NM $32 VAR_DEPS $4000 VAR_TYPE $1
|
||||
VAR_LOGIC $4000 VAR_FMT $32 VAR_LABEL $256
|
||||
_____DELETE__THIS__RECORD_____ $3;
|
||||
LIBREF='DCTEST';
|
||||
DSN='DC_DERIV';
|
||||
VAR_NM='LONG_LABEL';
|
||||
VAR_DEPS='';
|
||||
VAR_TYPE='N';
|
||||
VAR_LEN=8;
|
||||
VAR_FMT='';
|
||||
VAR_LOGIC='1';
|
||||
ACTIVE=1;
|
||||
VAR_LABEL=repeat('x',256);
|
||||
_____DELETE__THIS__RECORD_____='No';
|
||||
run;
|
||||
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputdatasets=work.sascontroltable work.jsdata,
|
||||
outlib=web10,
|
||||
outref=wb10,
|
||||
mdebug=&sasjs_mdebug
|
||||
)
|
||||
|
||||
%let abort10=0;
|
||||
proc sql noprint;
|
||||
select count(*) into: abort10
|
||||
from &dc_libref..mpe_loads
|
||||
where processed_dttm>&t10
|
||||
and reason_txt='A label over 256 characters should abort'
|
||||
and upcase(status) ne 'SUCCESS';
|
||||
quit;
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(&abort10>0),
|
||||
desc=Checking a label over 256 characters is rejected,
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/* tidy up so repeated runs start clean */
|
||||
proc sql noprint;
|
||||
delete from &dc_libref..mpe_derived
|
||||
|
||||
@@ -125,9 +125,18 @@ data work.derived_check;
|
||||
then reason='VAR_LEN exceeds 8 for a numeric variable';
|
||||
else if index(var_fmt,';')>0 or indexc(var_fmt,'&%')>0
|
||||
or index(var_fmt,'/*')>0 or index(var_fmt,'*/')>0
|
||||
then reason='VAR_FMT must be a format name, without ; & % or //*';
|
||||
then reason='VAR_FMT must be a format name, without ; & % or /* */';
|
||||
else if verify(strip(var_fmt),'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$.')>0
|
||||
then reason='VAR_FMT must be a format name';
|
||||
/* VAR_LABEL is free text: any character is allowed, in any script, and the
|
||||
quotes in it are escaped by %mp_derivedvars when the attrib statement is
|
||||
built. What is refused is anything that cannot be a single-line column
|
||||
label - a control character or a line break - and a value long enough to
|
||||
be cut down silently, since the engine truncates the label to 256. */
|
||||
else if anycntrl(var_label)>0
|
||||
then reason='VAR_LABEL must be plain text - no line breaks or control characters';
|
||||
else if klength(var_label)>256
|
||||
then reason='VAR_LABEL exceeds 256 characters';
|
||||
else if missing(var_logic)
|
||||
then reason='VAR_LOGIC is required';
|
||||
else if index(var_logic,';')>0
|
||||
@@ -443,7 +452,7 @@ run;
|
||||
|
||||
%mp_abort(iftrue= (&cyc_found=1)
|
||||
,mac=mpe_derived_postedit
|
||||
,msg=%str(VAR_DEPS contains a cycle: &cyc_path)
|
||||
,msg=%str(MPE_DERIVED: VAR_DEPS contains a cycle: &cyc_path)
|
||||
)
|
||||
|
||||
%mend _mpe_derived_postedit;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<h4> SAS Macros </h4>
|
||||
@li mp_assert.sas
|
||||
@li mx_execute.sas
|
||||
@li mf_getplatform.sas
|
||||
@li mf_getuniquefileref.sas
|
||||
|
||||
**/
|
||||
@@ -30,6 +31,11 @@ data _null_;
|
||||
put '%sysfunc(coalescec(&dclib..MPE_X_TEST,))';
|
||||
run;
|
||||
|
||||
/* Platform gate: the abort is asserted from the child job state
|
||||
(work.results, written by mv_jobflow), which only the Viya branch of
|
||||
%mx_execute produces. On the other platforms the state cannot be read,
|
||||
so the case is recorded as a skip rather than a failure. */
|
||||
%if %mf_getplatform()=SASVIYA %then %do;
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputfiles=&f1:iwant,
|
||||
@@ -48,6 +54,14 @@ run;
|
||||
desc=Macro content in libds aborts the service,
|
||||
outds=work.test_results
|
||||
)
|
||||
%end;
|
||||
%else %do;
|
||||
%mp_assert(
|
||||
iftrue=(1=1),
|
||||
desc=Macro content in libds aborts the service - skipped (the job state it reads is only written on Viya),
|
||||
outds=work.test_results
|
||||
)
|
||||
%end;
|
||||
|
||||
/**
|
||||
* Test 2 - valid libds still returns columns
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<h4> SAS Macros </h4>
|
||||
@li mp_assert.sas
|
||||
@li mx_execute.sas
|
||||
@li mf_getplatform.sas
|
||||
@li mf_getuniquefileref.sas
|
||||
|
||||
**/
|
||||
@@ -31,6 +32,11 @@ data _null_;
|
||||
put '%sysfunc(coalescec(&dclib..MPE_X_TEST,)),SOME_TIME';
|
||||
run;
|
||||
|
||||
/* Platform gate: the abort is asserted from the child job state
|
||||
(work.results, written by mv_jobflow), which only the Viya branch of
|
||||
%mx_execute produces. On the other platforms the state cannot be read,
|
||||
so the case is recorded as a skip rather than a failure. */
|
||||
%if %mf_getplatform()=SASVIYA %then %do;
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputfiles=&f1:iwant,
|
||||
@@ -49,6 +55,14 @@ run;
|
||||
desc=Macro content in libds aborts the service,
|
||||
outds=work.test_results
|
||||
)
|
||||
%end;
|
||||
%else %do;
|
||||
%mp_assert(
|
||||
iftrue=(1=1),
|
||||
desc=Macro content in libds aborts the service - skipped (the job state it reads is only written on Viya),
|
||||
outds=work.test_results
|
||||
)
|
||||
%end;
|
||||
|
||||
/**
|
||||
* Test 2 - macro content in col must abort the service
|
||||
@@ -60,6 +74,11 @@ data _null_;
|
||||
put '&dclib..MPE_X_TEST,%sysfunc(coalescec(SOME_TIME,))';
|
||||
run;
|
||||
|
||||
/* Platform gate: the abort is asserted from the child job state
|
||||
(work.results, written by mv_jobflow), which only the Viya branch of
|
||||
%mx_execute produces. On the other platforms the state cannot be read,
|
||||
so the case is recorded as a skip rather than a failure. */
|
||||
%if %mf_getplatform()=SASVIYA %then %do;
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputfiles=&f2:iwant,
|
||||
@@ -78,6 +97,14 @@ run;
|
||||
desc=Macro content in col aborts the service,
|
||||
outds=work.test_results
|
||||
)
|
||||
%end;
|
||||
%else %do;
|
||||
%mp_assert(
|
||||
iftrue=(1=1),
|
||||
desc=Macro content in col aborts the service - skipped (the job state it reads is only written on Viya),
|
||||
outds=work.test_results
|
||||
)
|
||||
%end;
|
||||
|
||||
/**
|
||||
* Test 3 - valid inputs still return values
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<h4> SAS Macros </h4>
|
||||
@li mp_assert.sas
|
||||
@li mx_execute.sas
|
||||
@li mf_getplatform.sas
|
||||
|
||||
**/
|
||||
|
||||
@@ -31,6 +32,11 @@ data work.params1;
|
||||
name='filter';value='0';output;
|
||||
run;
|
||||
|
||||
/* Platform gate: the abort is asserted from the child job state
|
||||
(work.results, written by mv_jobflow), which only the Viya branch of
|
||||
%mx_execute produces. On the other platforms the state cannot be read,
|
||||
so the case is recorded as a skip rather than a failure. */
|
||||
%if %mf_getplatform()=SASVIYA %then %do;
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputparams=work.params1,
|
||||
@@ -49,6 +55,14 @@ run;
|
||||
desc=Macro content in table aborts the service,
|
||||
outds=work.test_results
|
||||
)
|
||||
%end;
|
||||
%else %do;
|
||||
%mp_assert(
|
||||
iftrue=(1=1),
|
||||
desc=Macro content in table aborts the service - skipped (the job state it reads is only written on Viya),
|
||||
outds=work.test_results
|
||||
)
|
||||
%end;
|
||||
|
||||
/**
|
||||
* Test 2 - sql injection in filter must abort the service
|
||||
@@ -60,6 +74,11 @@ data work.params2;
|
||||
name='filter';value='0 or 1=1';output;
|
||||
run;
|
||||
|
||||
/* Platform gate: the abort is asserted from the child job state
|
||||
(work.results, written by mv_jobflow), which only the Viya branch of
|
||||
%mx_execute produces. On the other platforms the state cannot be read,
|
||||
so the case is recorded as a skip rather than a failure. */
|
||||
%if %mf_getplatform()=SASVIYA %then %do;
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputparams=work.params2,
|
||||
@@ -78,6 +97,14 @@ run;
|
||||
desc=SQL injection in filter aborts the service,
|
||||
outds=work.test_results
|
||||
)
|
||||
%end;
|
||||
%else %do;
|
||||
%mp_assert(
|
||||
iftrue=(1=1),
|
||||
desc=SQL injection in filter aborts the service - skipped (the job state it reads is only written on Viya),
|
||||
outds=work.test_results
|
||||
)
|
||||
%end;
|
||||
|
||||
/**
|
||||
* Test 3 - path traversal in table must abort the service
|
||||
@@ -91,6 +118,11 @@ data work.params3;
|
||||
name='filter';value='0';output;
|
||||
run;
|
||||
|
||||
/* Platform gate: the abort is asserted from the child job state
|
||||
(work.results, written by mv_jobflow), which only the Viya branch of
|
||||
%mx_execute produces. On the other platforms the state cannot be read,
|
||||
so the case is recorded as a skip rather than a failure. */
|
||||
%if %mf_getplatform()=SASVIYA %then %do;
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputparams=work.params3,
|
||||
@@ -109,6 +141,14 @@ run;
|
||||
desc=Path traversal in table aborts the service,
|
||||
outds=work.test_results
|
||||
)
|
||||
%end;
|
||||
%else %do;
|
||||
%mp_assert(
|
||||
iftrue=(1=1),
|
||||
desc=Path traversal in table aborts the service - skipped (the job state it reads is only written on Viya),
|
||||
outds=work.test_results
|
||||
)
|
||||
%end;
|
||||
|
||||
/**
|
||||
* Test 4 - the valid request must still work (positive control)
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<h4> SAS Macros </h4>
|
||||
@li mp_assert.sas
|
||||
@li mx_execute.sas
|
||||
@li mf_getplatform.sas
|
||||
@li mf_getuniquefileref.sas
|
||||
|
||||
**/
|
||||
@@ -30,6 +31,11 @@ data _null_;
|
||||
put '%sysfunc(coalescec(&dclib..MPE_TABLES,))';
|
||||
run;
|
||||
|
||||
/* Platform gate: the abort is asserted from the child job state
|
||||
(work.results, written by mv_jobflow), which only the Viya branch of
|
||||
%mx_execute produces. On the other platforms the state cannot be read,
|
||||
so the case is recorded as a skip rather than a failure. */
|
||||
%if %mf_getplatform()=SASVIYA %then %do;
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputfiles=&f1:iwant,
|
||||
@@ -48,6 +54,14 @@ run;
|
||||
desc=Macro content in filter_table aborts the service,
|
||||
outds=work.test_results
|
||||
)
|
||||
%end;
|
||||
%else %do;
|
||||
%mp_assert(
|
||||
iftrue=(1=1),
|
||||
desc=Macro content in filter_table aborts the service - skipped (the job state it reads is only written on Viya),
|
||||
outds=work.test_results
|
||||
)
|
||||
%end;
|
||||
|
||||
/**
|
||||
* Test 2 - valid filter_table still stores a filter
|
||||
|
||||
Reference in new issue
Block a user