Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93ef9228aa | ||
|
|
bcad05e032 | ||
|
|
dbc3044a0f | ||
|
|
1fc7b55e3b | ||
|
|
bcb8fde986 | ||
|
|
a20ce96744 | ||
|
|
55fd16d53c | ||
|
|
1385741f2d | ||
|
|
05126ae73a | ||
|
|
3a6a216b9d | ||
|
|
edac9a987a | ||
|
|
8169ef253d | ||
|
|
f2535ce335 | ||
|
|
831f0b27a9 | ||
|
|
f08c4c231f | ||
|
|
b505e45c6f | ||
|
|
3de6655f26 | ||
|
|
6e5093858b | ||
|
|
b6b00ab96e | ||
|
|
e49e8e6710 | ||
|
|
1354fced46 | ||
|
|
a0c9349e08 | ||
|
|
644a7eb33d | ||
|
|
c019224e96 | ||
|
|
05f3f49e0c |
No files matched your search
@@ -15,6 +15,22 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Scan for secrets (gitleaks)
|
||||
# The local pre-commit hook is suppressed entirely when .npmrc sets
|
||||
# ignore-scripts=true (the prepare script that would activate the
|
||||
# hooks never runs), so CI is the enforcement point: a leaked
|
||||
# credential must fail the build, not just a commit.
|
||||
# Uses the same @nogoo9/gitleaks binary the pre-commit hook pins - the
|
||||
# gitleaks-action requires a license key for organization repos.
|
||||
# Runs before client/.npmrc is written below, because that file is
|
||||
# created from the NPMRC secret and a later scan would flag the
|
||||
# registry token this job injects itself.
|
||||
# --no-git scans the working tree, which is what a pull request gate
|
||||
# is about.
|
||||
run: |
|
||||
npm ci
|
||||
node_modules/.bin/gitleaks detect --no-git --source . --redact --no-banner -v
|
||||
|
||||
- name: Install Google Chrome
|
||||
run: |
|
||||
apt-get update
|
||||
@@ -126,8 +142,27 @@ jobs:
|
||||
echo NODE_PATH=node >> .env
|
||||
echo CORS=enable >> .env
|
||||
echo WHITELIST=http://localhost:4200 >> .env
|
||||
# The server refuses to run as root unless RUN_AS names an account
|
||||
# (a security change in the recent releases). This job runs as root
|
||||
# in a throwaway container, so opt in explicitly.
|
||||
echo RUN_AS=root >> .env
|
||||
cat .env
|
||||
pm2 start api-linux --wait-ready
|
||||
# `pm2 start --wait-ready` can return while the app is still
|
||||
# launching (it does not always wait for the ready signal), and the
|
||||
# next step hits :5000 immediately. Wait for the server to answer,
|
||||
# and dump its log if it never does.
|
||||
for i in $(seq 1 60); do
|
||||
if curl -sf -o /dev/null http://localhost:5000/; then
|
||||
echo "SASjs server answered on attempt ${i}"
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "SASjs server did not come up within 120s"
|
||||
pm2 list
|
||||
pm2 logs api-linux --lines 100 --nostream
|
||||
exit 1
|
||||
|
||||
- name: Deploy mocked services
|
||||
run: |
|
||||
@@ -163,7 +198,7 @@ jobs:
|
||||
# Start frontend and run cypress
|
||||
# timeout 1800: SIGTERM after 30 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"
|
||||
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"
|
||||
|
||||
- name: Zip Cypress videos
|
||||
if: always()
|
||||
|
||||
@@ -33,10 +33,29 @@ jobs:
|
||||
echo NODE_PATH=node >> .env
|
||||
echo CORS=enable >> .env
|
||||
echo WHITELIST=http://localhost:4200 >> .env
|
||||
# The server refuses to run as root unless RUN_AS names an account
|
||||
# (a security change in the recent releases). This job runs as root
|
||||
# in a throwaway container, so opt in explicitly.
|
||||
echo RUN_AS=root >> .env
|
||||
cat .env
|
||||
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
||||
unzip linux.zip
|
||||
pm2 start api-linux --wait-ready
|
||||
# `pm2 start --wait-ready` can return while the app is still
|
||||
# launching (it does not always wait for the ready signal), and the
|
||||
# next step hits :5000 immediately. Wait for the server to answer,
|
||||
# and dump its log if it never does.
|
||||
for i in $(seq 1 60); do
|
||||
if curl -sf -o /dev/null http://localhost:5000/; then
|
||||
echo "SASjs server answered on attempt ${i}"
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "SASjs server did not come up within 120s"
|
||||
pm2 list
|
||||
pm2 logs api-linux --lines 100 --nostream
|
||||
exit 1
|
||||
|
||||
- name: Write .npmrc file
|
||||
run: echo "$NPMRC" > client/.npmrc
|
||||
|
||||
@@ -117,8 +117,27 @@ jobs:
|
||||
echo NODE_PATH=node >> .env
|
||||
echo CORS=enable >> .env
|
||||
echo WHITELIST=http://localhost:4200 >> .env
|
||||
# The server refuses to run as root unless RUN_AS names an account
|
||||
# (a security change in the recent releases). This job runs as root
|
||||
# in a throwaway container, so opt in explicitly.
|
||||
echo RUN_AS=root >> .env
|
||||
cat .env
|
||||
pm2 start api-linux --wait-ready
|
||||
# `pm2 start --wait-ready` can return while the app is still
|
||||
# launching (it does not always wait for the ready signal), and the
|
||||
# next step hits :5000 immediately. Wait for the server to answer,
|
||||
# and dump its log if it never does.
|
||||
for i in $(seq 1 60); do
|
||||
if curl -sf -o /dev/null http://localhost:5000/; then
|
||||
echo "SASjs server answered on attempt ${i}"
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "SASjs server did not come up within 120s"
|
||||
pm2 list
|
||||
pm2 logs api-linux --lines 100 --nostream
|
||||
exit 1
|
||||
|
||||
- name: Deploy mocked services
|
||||
run: |
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Hooks for the pre-commit framework - https://pre-commit.com
|
||||
#
|
||||
# The repo also ships native hooks in .git-hooks/, covering the secret scan, a
|
||||
# 2MB commit size limit, a prettier check and Conventional Commits. But .npmrc
|
||||
# sets ignore-scripts=true, so the `prepare` script that points git at them
|
||||
# never runs on `npm i` - a fresh clone commits unchecked until the one-time
|
||||
# command in CONTRIBUTING.md is run by hand.
|
||||
#
|
||||
# This file is the standard route instead: install the framework once with
|
||||
# `pre-commit install`, and the secret scan runs on every commit. The hook is
|
||||
# pinned to an exact gitleaks release tag.
|
||||
repos:
|
||||
- repo: https://github.com/gitleaks/gitleaks
|
||||
rev: v8.30.1
|
||||
hooks:
|
||||
- id: gitleaks
|
||||
@@ -19,6 +19,8 @@ git config core.hooksPath ./.git-hooks
|
||||
|
||||
The pre-commit hook requires the gitleaks binary provided by the root `@nogoo9/gitleaks` devDependency, so make sure `npm i` has run in the repo root before your first commit.
|
||||
|
||||
The standard [pre-commit](https://pre-commit.com) route is available as well, and does not depend on the lifecycle scripts above: install it once with `pre-commit install` and the secret scan in [`.pre-commit-config.yaml`](./.pre-commit-config.yaml) runs on every commit. The same scan also runs in CI, so a leaked credential fails the build regardless of which local hooks are active.
|
||||
|
||||
## Dependencies that requires licences
|
||||
|
||||
[SheetJS Pro Version](https://www.npmjs.com/package/sheetjs)
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
export {}
|
||||
|
||||
const hostUrl = Cypress.env('hosturl')
|
||||
const appLocation = Cypress.env('appLocation')
|
||||
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||
|
||||
context('Export DC Library DDL: ', function () {
|
||||
this.beforeAll(() => {
|
||||
cy.loginAndUpdateValidKey(true)
|
||||
})
|
||||
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
visitPage('home')
|
||||
})
|
||||
|
||||
it('1 | the admin action opens the flavour chooser and exports the SAS flavour by default', () => {
|
||||
visitPage('system')
|
||||
|
||||
cy.get('.admin-action', { timeout: longerCommandTimeout })
|
||||
.contains('Export DC Library DDL')
|
||||
.should('exist')
|
||||
cy.get('.admin-action').contains('button', 'EXPORT').should('exist')
|
||||
|
||||
cy.window().then((win) => {
|
||||
cy.stub(win, 'open').as('open')
|
||||
})
|
||||
|
||||
cy.get('.admin-action').contains('button', 'EXPORT').click()
|
||||
|
||||
const ddlModal = () => cy.contains('clr-modal', 'Export DC Library DDL')
|
||||
|
||||
ddlModal().should('be.visible')
|
||||
ddlModal().within(() => {
|
||||
cy.get('select').should('have.value', 'SAS')
|
||||
// the schema box only applies to the DB flavours
|
||||
cy.contains('label', 'Schema (optional)').should('not.exist')
|
||||
})
|
||||
|
||||
ddlModal().contains('button', 'Export').click()
|
||||
|
||||
cy.get('@open').should(
|
||||
'have.been.calledWithMatch',
|
||||
/\/services\/admin\/exportdb&flavour=SAS$/
|
||||
)
|
||||
})
|
||||
|
||||
it('2 | the PGSQL flavour reveals the optional schema box and passes it to the service', () => {
|
||||
visitPage('system')
|
||||
|
||||
cy.window().then((win) => {
|
||||
cy.stub(win, 'open').as('open')
|
||||
})
|
||||
|
||||
cy.get('.admin-action').contains('button', 'EXPORT').click()
|
||||
|
||||
const ddlModal = () => cy.contains('clr-modal', 'Export DC Library DDL')
|
||||
|
||||
ddlModal().within(() => {
|
||||
cy.get('select').select('PGSQL')
|
||||
cy.contains('label', 'Schema (optional)').should('be.visible')
|
||||
cy.get('input[type="text"]').clear().type('DC')
|
||||
})
|
||||
|
||||
ddlModal().contains('button', 'Export').click()
|
||||
|
||||
cy.get('@open').should(
|
||||
'have.been.calledWithMatch',
|
||||
/\/services\/admin\/exportdb&flavour=PGSQL&schema=DC$/
|
||||
)
|
||||
})
|
||||
|
||||
it('3 | the TSQL flavour exports without a schema when the box is left empty', () => {
|
||||
visitPage('system')
|
||||
|
||||
cy.window().then((win) => {
|
||||
cy.stub(win, 'open').as('open')
|
||||
})
|
||||
|
||||
cy.get('.admin-action').contains('button', 'EXPORT').click()
|
||||
|
||||
const ddlModal = () => cy.contains('clr-modal', 'Export DC Library DDL')
|
||||
|
||||
ddlModal().within(() => {
|
||||
cy.get('select').select('TSQL')
|
||||
cy.contains('label', 'Schema (optional)').should('be.visible')
|
||||
})
|
||||
|
||||
ddlModal().contains('button', 'Export').click()
|
||||
|
||||
cy.get('@open').should(
|
||||
'have.been.calledWithMatch',
|
||||
/\/services\/admin\/exportdb&flavour=TSQL$/
|
||||
)
|
||||
})
|
||||
|
||||
it('4 | the SAS export streams the DC library DDL', () => {
|
||||
visitPage('system')
|
||||
|
||||
cy.window().then((win) => {
|
||||
cy.stub(win, 'open').as('open')
|
||||
})
|
||||
|
||||
cy.get('.admin-action').contains('button', 'EXPORT').click()
|
||||
|
||||
const ddlModal = () => cy.contains('clr-modal', 'Export DC Library DDL')
|
||||
|
||||
ddlModal().contains('button', 'Export').click()
|
||||
|
||||
// the URL the action opens is served by the backend - against the mocks
|
||||
// that is services/admin/exportdb.js, which renders the library as basic
|
||||
// DDL per flavour
|
||||
cy.get('@open').then((open: any) => {
|
||||
const url = open.getCall(0).args[0]
|
||||
expect(url).to.match(/\/services\/admin\/exportdb&flavour=SAS$/)
|
||||
|
||||
cy.request(url).then((res) => {
|
||||
expect(res.status).to.eq(200)
|
||||
expect(res.headers['content-disposition']).to.contain('DC_JSLIB.ddl')
|
||||
expect(res.body).to.contain('create table DC_JSLIB.MPE_CONFIG(')
|
||||
expect(res.body).to.contain('VAR_NAME char(32)')
|
||||
expect(res.body).to.contain('create table DC_JSLIB.MPE_X_TEST(')
|
||||
// the SAS flavour has no schema statement and no unique index
|
||||
expect(res.body).not.to.contain('CREATE SCHEMA')
|
||||
expect(res.body).not.to.contain('UNIQUE INDEX')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('5 | the PGSQL export honours the schema and quotes its identifiers', () => {
|
||||
visitPage('system')
|
||||
|
||||
cy.window().then((win) => {
|
||||
cy.stub(win, 'open').as('open')
|
||||
})
|
||||
|
||||
cy.get('.admin-action').contains('button', 'EXPORT').click()
|
||||
|
||||
const ddlModal = () => cy.contains('clr-modal', 'Export DC Library DDL')
|
||||
|
||||
ddlModal().within(() => {
|
||||
cy.get('select').select('PGSQL')
|
||||
cy.get('input[type="text"]').clear().type('DC')
|
||||
})
|
||||
ddlModal().contains('button', 'Export').click()
|
||||
|
||||
cy.get('@open').then((open: any) => {
|
||||
const url = open.getCall(0).args[0]
|
||||
expect(url).to.match(/&flavour=PGSQL&schema=DC$/)
|
||||
|
||||
cy.request(url).then((res) => {
|
||||
expect(res.status).to.eq(200)
|
||||
expect(res.body).to.contain('CREATE SCHEMA DC;')
|
||||
expect(res.body).to.contain('CREATE TABLE DC.MPE_CONFIG (')
|
||||
expect(res.body).to.contain('"VAR_NAME" VARCHAR(32)')
|
||||
expect(res.body).to.contain('CREATE UNIQUE INDEX "MPE_CONFIG_pk"')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const visitPage = (url: string) => {
|
||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
// The EDIT screen refuses a selection that is too large - DC_MAXOBS_WEBEDIT
|
||||
// rows, or DC_MAXCELLS_WEBEDIT cells (rows x columns), whichever is reached
|
||||
// first - and the VIEW screen caps at DC_MAXOBS_WEBVIEW rows.
|
||||
//
|
||||
// This spec pins those limits against the mock, which mirrors the shipped
|
||||
// defaults (250 rows / 200000 cells for EDIT, 2000 rows for VIEW). It uses
|
||||
// two existing fixtures rather than new ones:
|
||||
// MPE_X_SEARCH - 1000 rows x 9 cols, so it trips the ROW limit
|
||||
// MPE_X_WIDE - 200 rows x 1001 cols (200,400 cells), under the row limit
|
||||
// but over the CELL limit
|
||||
export {}
|
||||
|
||||
const hostUrl = Cypress.env('hosturl')
|
||||
const appLocation = Cypress.env('appLocation')
|
||||
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||
const serverType = Cypress.env('serverType')
|
||||
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
|
||||
|
||||
context('EDIT row and cell limits, VIEW row limit: ', function () {
|
||||
this.beforeAll(() => {
|
||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
// forceLicenceKey=true: applies a valid licence rather than relying on an
|
||||
// earlier spec in the run having done so (see commands.ts).
|
||||
cy.loginAndUpdateValidKey(true)
|
||||
})
|
||||
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
visitPage('home')
|
||||
visitPage('view/data')
|
||||
})
|
||||
|
||||
it('1 | VIEW renders up to the raised row cap, past the old 500', () => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_search')
|
||||
|
||||
// Neither the header nor the cell text can pin the cap: the header reports
|
||||
// the table's true row count whether or not the grid is capped, and the
|
||||
// fixture's own numeric columns carry values above 1500 (DEPTH_M reaches
|
||||
// 4293, SAMPLE_COUNT 4210), so a text match can hit fixture data.
|
||||
//
|
||||
// The PK can pin it. The first td of a .ht_master row is the PK - the row
|
||||
// header, where present, is a th - and MPE_X_SEARCH holds PK 1001-2000, so
|
||||
// the range check below is what stops a mis-selected column passing. With
|
||||
// the old 500 row cap the grid stopped at PK 1500, so a rendered PK above
|
||||
// 1500 can only appear if the cap was raised.
|
||||
scrollGridToBottom()
|
||||
cy.get('#hotTable .ht_master .htCore tbody tr', {
|
||||
timeout: longerCommandTimeout
|
||||
}).should(($rows) => {
|
||||
const pks = $rows
|
||||
.toArray()
|
||||
.map((row) => row.querySelectorAll('td')[0])
|
||||
.filter((td) => !!td)
|
||||
.map((td) => parseInt((td.textContent || '').trim(), 10))
|
||||
.filter((pk) => !isNaN(pk))
|
||||
|
||||
expect(pks.length).to.be.greaterThan(0)
|
||||
pks.forEach((pk) => expect(pk).to.be.within(1001, 2000))
|
||||
expect(Math.max(...pks)).to.be.greaterThan(1500)
|
||||
})
|
||||
})
|
||||
|
||||
it('2 | EDIT refuses a table over the row limit', () => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_search')
|
||||
openEdit()
|
||||
|
||||
cy.get('.abortMsg', { timeout: longerCommandTimeout }).should(
|
||||
'contain.text',
|
||||
'Table is too big (1000 rows) - please filter and try again!'
|
||||
)
|
||||
})
|
||||
|
||||
it('3 | EDIT refuses a table over the cell limit', () => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_wide')
|
||||
openEdit()
|
||||
|
||||
// 200 rows x 1002 columns (the 1001 stored columns plus the housekeeping
|
||||
// delete column) = 200,400 cells, over the 200,000 cell limit, while the
|
||||
// row count is under the 250 row limit.
|
||||
cy.get('.abortMsg', { timeout: longerCommandTimeout }).should(
|
||||
'contain.text',
|
||||
'Selection is too wide (200 rows x 1002 cols) - please filter and try again!'
|
||||
)
|
||||
})
|
||||
|
||||
it('4 | the EDIT limits follow MPE_CONFIG, not the shipped defaults', () => {
|
||||
// Two things at once. The seeded config carries the shipped defaults (250
|
||||
// rows / 200000 cells), which are the numbers the mock used to hard-code -
|
||||
// so nothing above would notice if the mock stopped reading MPE_CONFIG at
|
||||
// all. And MPE_CONFIG lives in the control library, so reading it from the
|
||||
// table's own library would fail for this table in particular.
|
||||
//
|
||||
// The cell limit is the one to move: 7 rows x 18 columns is 126 cells, so
|
||||
// at 110 the TESTDATA.MPE_X_NEW edit must be refused, where a hard-coded
|
||||
// 200000 - or a config read that fails and falls back to it - lets it
|
||||
// through. 110 is still above MPE_CONFIG's own 17 x 6 = 102, so the option
|
||||
// can be put back afterwards.
|
||||
setConfigValue('DC_MAXCELLS_WEBEDIT', '110', () => {
|
||||
visitPage('view/data/TESTDATA.MPE_X_NEW')
|
||||
closeAbortModalIfPresent()
|
||||
openEdit()
|
||||
|
||||
cy.get('.abortMsg', { timeout: longerCommandTimeout }).should(
|
||||
'contain.text',
|
||||
'Selection is too wide (7 rows x 18 cols) - please filter and try again!'
|
||||
)
|
||||
})
|
||||
|
||||
// Put the option back, so the rest of the estate sees the shipped default.
|
||||
setConfigValue('DC_MAXCELLS_WEBEDIT', '200000', () => {
|
||||
visitPage('view/data/TESTDATA.MPE_X_NEW')
|
||||
closeAbortModalIfPresent()
|
||||
openEdit()
|
||||
|
||||
// 126 cells under a 200000 cell limit: the editor loads, no abort.
|
||||
cy.get('#hotTable .ht_master .htCore tbody tr', {
|
||||
timeout: longerCommandTimeout
|
||||
}).should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
it('5 | the filter picker still lists the variables after the abort', () => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_search')
|
||||
openEdit()
|
||||
|
||||
cy.get('.abortMsg', { timeout: longerCommandTimeout }).should(
|
||||
'contain.text',
|
||||
'Table is too big (1000 rows) - please filter and try again!'
|
||||
)
|
||||
|
||||
closeAbortModal()
|
||||
|
||||
// The abort returns no table, and the picker's variable list normally comes
|
||||
// from the table's own columns - so without the public/getcols fallback the
|
||||
// "please filter and try again" advice has nothing to pick from. Every
|
||||
// column of MPE_X_SEARCH must be on offer.
|
||||
openFilterPopup()
|
||||
|
||||
filterVariables().should(($options) => {
|
||||
const names = $options
|
||||
.toArray()
|
||||
.map((o) => (o.textContent || '').trim())
|
||||
.filter((n) => n.length > 0)
|
||||
|
||||
expect(names, 'the picker offers the table columns').to.include.members([
|
||||
'PRIMARY_KEY_FIELD',
|
||||
'SITE_NAME',
|
||||
'VESSEL',
|
||||
'SAMPLE_COUNT',
|
||||
'DEPTH_M',
|
||||
'SPECIES',
|
||||
'CRUISE_DATE',
|
||||
'EXPEDITION_ID',
|
||||
'NOTES'
|
||||
])
|
||||
})
|
||||
|
||||
// and the suggestion list is usable, not just populated
|
||||
setFilterVariable('SITE_NAME')
|
||||
|
||||
// Submitting is what exercises TYPE. The picker keys its operator set and
|
||||
// its value quoting on it, and the fallback list never passes through
|
||||
// mergeColsRules, so without TYPE on this payload a character value would be
|
||||
// submitted unquoted and the backend's %mp_filtercheck would reject it.
|
||||
setFilterValue('Bristol')
|
||||
submitFilter()
|
||||
|
||||
cy.get('.infoBar-text', { timeout: longerCommandTimeout }).should(
|
||||
'contain.text',
|
||||
"SITE_NAME = 'Bristol'"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const visitPage = (url: string) => {
|
||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
||||
}
|
||||
|
||||
// The viewer's options dropdown holds the Edit item; it only renders for a
|
||||
// table that is registered in MPE_TABLES (tableEditExists()).
|
||||
const openEdit = () => {
|
||||
cy.get('.filterSide', { timeout: longerCommandTimeout }).click()
|
||||
cy.contains('[clrDropdownItem]', 'Edit', {
|
||||
timeout: longerCommandTimeout
|
||||
}).click()
|
||||
}
|
||||
|
||||
/** The abort modal offers Close, and the filter button sits behind it. */
|
||||
const closeAbortModal = () => {
|
||||
cy.get('app-info-modal', { timeout: longerCommandTimeout })
|
||||
.contains('button', 'Close')
|
||||
.click()
|
||||
}
|
||||
|
||||
/**
|
||||
* The EDIT screen's Filter button opens the same .filter-modal the viewer uses.
|
||||
* It only renders once the editor is in its read-only (non-editing) state,
|
||||
* which is where a failed EDIT load leaves it.
|
||||
*/
|
||||
const openFilterPopup = () => {
|
||||
cy.contains('button.btnView', 'Filter', {
|
||||
timeout: longerCommandTimeout
|
||||
}).click()
|
||||
cy.get('.filter-modal', { timeout: longerCommandTimeout }).should(
|
||||
'be.visible'
|
||||
)
|
||||
}
|
||||
|
||||
/** The picker's variable suggestion list. */
|
||||
const filterVariables = () =>
|
||||
cy.get('#datalist_vals_var_id0_0 option', { timeout: longerCommandTimeout })
|
||||
|
||||
/** The soft-select drops a transparent overlay over the modal, so it needs force. */
|
||||
const setFilterVariable = (column: string) => {
|
||||
cy.get('#vals_var_id0_0').clear({ force: true }).type(column, { force: true })
|
||||
cy.get('#datalist_vals_var_id0_0 option').contains(column).click({
|
||||
force: true
|
||||
})
|
||||
cy.get('#vals_var_id0_0').trigger('keyup', { key: 'Escape', force: true })
|
||||
}
|
||||
|
||||
/** The value input of the same soft-select row. */
|
||||
const setFilterValue = (value: string) => {
|
||||
cy.get('#vals_0_0').clear({ force: true }).type(value, { force: true })
|
||||
cy.get('#vals_0_0').trigger('keyup', { key: 'Escape', force: true })
|
||||
cy.wait(400)
|
||||
}
|
||||
|
||||
const submitFilter = () => {
|
||||
cy.contains('.filter-modal button', 'Ok').click()
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout }).should('not.exist')
|
||||
cy.wait(2500)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a DC option in MPE_CONFIG through the editor and approves it, then hands
|
||||
* back to the callback. The same route licensing.cy.ts uses for the licence
|
||||
* keys: the config table needs an approval like any other.
|
||||
*/
|
||||
const setConfigValue = (
|
||||
varName: string,
|
||||
varValue: string,
|
||||
callback: () => void
|
||||
) => {
|
||||
reloadApp()
|
||||
// straight to the config table's editor, so the flow does not depend on
|
||||
// whichever view mode the previous step left behind
|
||||
visitPage('editor/DC_JSLIB.MPE_CONFIG')
|
||||
closeAbortModalIfPresent()
|
||||
clickOnEdit()
|
||||
|
||||
editTableField([{ varName, varValue }], () => {
|
||||
submitTable(() => {
|
||||
cy.wait(2000)
|
||||
approveTable(() => {
|
||||
reloadApp()
|
||||
callback()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* A full page load. A visit that only changes the hash does not reload the
|
||||
* app, so an open modal - the abort message from the step just gone, say -
|
||||
* would still be covering the page.
|
||||
*/
|
||||
const reloadApp = () => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout }).should('not.exist')
|
||||
}
|
||||
|
||||
/**
|
||||
* DC puts abort messages up as modals over whatever page is showing, and they
|
||||
* outlive a hash-only navigation, so clear one before driving the page.
|
||||
*/
|
||||
const closeAbortModalIfPresent = () => {
|
||||
cy.get('body').then(($body) => {
|
||||
if ($body.find('app-info-modal').length > 0) {
|
||||
cy.get('app-info-modal')
|
||||
.contains('button', 'Close')
|
||||
.click({ force: true })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* The viewer's Edit button. The options dropdown (.filterSide) that openEdit()
|
||||
* uses is not rendered for every table, so the config table is opened the way
|
||||
* licensing.cy.ts opens it.
|
||||
*/
|
||||
const clickOnEdit = (callback?: () => void) => {
|
||||
cy.get('.btnCtrl button.btn-primary', { timeout: longerCommandTimeout })
|
||||
.click()
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
}
|
||||
|
||||
/** Types a new value into the row whose cell carries varName. */
|
||||
const editTableField = (
|
||||
edits: { varName: string; varValue: string }[],
|
||||
callback: () => void
|
||||
) => {
|
||||
cy.get('#hotTable .ht_master .htCore tbody td', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then(($tds: any) => {
|
||||
const nodes = $tds.toArray()
|
||||
|
||||
for (const edit of edits) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
// exact match on the cell that holds the option name; the cell after it
|
||||
// is the value
|
||||
if ((nodes[i].textContent || '').trim() === edit.varName) {
|
||||
cy.wrap(nodes[i + 1])
|
||||
.dblclick()
|
||||
.then(() => {
|
||||
cy.focused()
|
||||
.type('{selectall}')
|
||||
.type(edit.varValue)
|
||||
.type('{enter}')
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback()
|
||||
})
|
||||
}
|
||||
|
||||
const submitTable = (callback: () => void) => {
|
||||
cy.get('.btnCtrl button.btn-primary', { timeout: longerCommandTimeout })
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get(".modal.ng-star-inserted button[type='submit']").click()
|
||||
callback()
|
||||
})
|
||||
}
|
||||
|
||||
const approveTable = (callback: () => void) => {
|
||||
cy.get('button', { timeout: longerCommandTimeout })
|
||||
.should('contain', 'Approve')
|
||||
.then((allButtons: any) => {
|
||||
for (const approvalButton of allButtons) {
|
||||
if (approvalButton.innerText.toLowerCase().includes('approve')) {
|
||||
approvalButton.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cy.get('button#acceptBtn', { timeout: longerCommandTimeout })
|
||||
.should('exist')
|
||||
.should('not.be.disabled')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('app-history', { timeout: longerCommandTimeout }).should(
|
||||
'exist'
|
||||
)
|
||||
callback()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Handsontable virtualises rows, so the last row is only in the DOM once the
|
||||
// grid has been scrolled to the bottom. The scroller is the .wtHolder inside
|
||||
// the .ht_master pane.
|
||||
const scrollGridToBottom = () => {
|
||||
cy.get('#hotTable .ht_master .wtHolder', { timeout: longerCommandTimeout })
|
||||
.scrollTo('bottom', { duration: 0 })
|
||||
.trigger('scroll')
|
||||
}
|
||||
|
||||
const openTableFromTree = (libNameIncludes: string, tablename: string) => {
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then((treeNodes: any) => {
|
||||
let targetLib
|
||||
|
||||
for (let node of treeNodes) {
|
||||
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
|
||||
targetLib = node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cy.get(targetLib).within(() => {
|
||||
// Small settle wait: right after a fresh visit the tree component can
|
||||
// still be re-rendering, causing this node to be found then swapped
|
||||
// out mid-click.
|
||||
cy.wait(300)
|
||||
|
||||
cy.get(
|
||||
'.clr-tree-node-content-container .clr-treenode-content p'
|
||||
).click()
|
||||
|
||||
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
||||
for (let innerNode of innerNodes) {
|
||||
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
||||
innerNode.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('#hotTable .ht_clone_top .htCore thead button.changeType', {
|
||||
timeout: longerCommandTimeout
|
||||
}).should('exist')
|
||||
}
|
||||
@@ -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@^16.0.1;handsontable@16.2.0;handsontable@17.1.0;handsontable@18.0.0;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@^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'
|
||||
},
|
||||
(error, json) => {
|
||||
if (error) {
|
||||
|
||||
Generated
+271
-469
File diff suppressed because it is too large.
Load diff
+13
-13
@@ -41,21 +41,21 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^20.3.27",
|
||||
"@angular/animations": "^20.3.33",
|
||||
"@angular/cdk": "^20.2.14",
|
||||
"@angular/common": "^20.3.27",
|
||||
"@angular/compiler": "^20.3.27",
|
||||
"@angular/core": "^20.3.27",
|
||||
"@angular/forms": "^20.3.27",
|
||||
"@angular/platform-browser": "^20.3.27",
|
||||
"@angular/platform-browser-dynamic": "^20.3.27",
|
||||
"@angular/router": "^20.3.27",
|
||||
"@angular/common": "^20.3.33",
|
||||
"@angular/compiler": "^20.3.33",
|
||||
"@angular/core": "^20.3.33",
|
||||
"@angular/forms": "^20.3.33",
|
||||
"@angular/platform-browser": "^20.3.33",
|
||||
"@angular/platform-browser-dynamic": "^20.3.33",
|
||||
"@angular/router": "^20.3.33",
|
||||
"@cds/core": "^6.15.1",
|
||||
"@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.0.0",
|
||||
"@sasjs/adapter": "^4.18.0",
|
||||
"@handsontable/angular-wrapper": "18.1.1",
|
||||
"@sasjs/adapter": "^4.19.1",
|
||||
"@sasjs/utils": "^3.6.0",
|
||||
"@sheet/crypto": "file:libraries/sheet-crypto.tgz",
|
||||
"@types/d3-graphviz": "^2.6.7",
|
||||
@@ -69,7 +69,7 @@
|
||||
"d3-graphviz": "^5.0.2",
|
||||
"exceljs": "^4.4.0",
|
||||
"fs-extra": "^7.0.1",
|
||||
"handsontable": "18.0.0",
|
||||
"handsontable": "18.1.1",
|
||||
"https-browserify": "1.0.0",
|
||||
"hyperformula": "^2.5.0",
|
||||
"iconv-lite": "^0.5.0",
|
||||
@@ -100,7 +100,7 @@
|
||||
"@angular-eslint/schematics": "19.8.1",
|
||||
"@angular-eslint/template-parser": "19.8.1",
|
||||
"@angular/cli": "^20.3.32",
|
||||
"@angular/compiler-cli": "^20.3.27",
|
||||
"@angular/compiler-cli": "^20.3.33",
|
||||
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
||||
"@compodoc/compodoc": "^2.0.0",
|
||||
"@cypress/webpack-preprocessor": "^5.17.1",
|
||||
@@ -148,7 +148,7 @@
|
||||
"ajv": "8.18.0",
|
||||
"uuid": "11.1.1",
|
||||
"lighthouse": "13.4.0",
|
||||
"fast-uri": "3.1.7",
|
||||
"fast-uri": "3.1.8",
|
||||
"readdir-glob": {
|
||||
"brace-expansion": "^5.0.9"
|
||||
},
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
|
||||
[class*=ht-theme-classic] .htDropdownMenu table tbody tr td.htSubmenu .htItemWrapper::after,
|
||||
[class*=ht-theme-classic] .htContextMenu table tbody tr td.htSubmenu .htItemWrapper::after,
|
||||
[class*=ht-theme-classic] .htFiltersConditionsMenu table tbody tr td.htSubmenu .htItemWrapper::after,
|
||||
[class*=ht-theme-classic] .pika-single .pika-next {
|
||||
[class*=ht-theme-classic] .htFiltersConditionsMenu table tbody tr td.htSubmenu .htItemWrapper::after {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
@@ -12,14 +11,6 @@
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] .pika-single .pika-prev {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/arrow-left.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] .ht-page-size-section__select-wrapper::after {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
|
||||
@@ -1206,7 +1206,16 @@ export class EditorComponent
|
||||
this.cdf.detectChanges()
|
||||
|
||||
this.submitLoading = false
|
||||
this.sasStoreService.setQueryVariables(this.libds, this.cols)
|
||||
// The columns that came with the table are the cheapest source for the
|
||||
// picker. When the table was not returned - the row/cell limits abort the
|
||||
// request before the data arrives - that list is empty, so the store
|
||||
// falls back to public/getcols rather than leaving the picker with
|
||||
// nothing to pick from.
|
||||
this.sasStoreService
|
||||
.setQueryVariablesFromTable(this.libds, this.cols)
|
||||
.catch((err: any) =>
|
||||
this.eventService.catchResponseError('public/getcols', err)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -417,9 +417,14 @@ describe('formulas plugin must not be disabled/re-enabled around a table load',
|
||||
* capping maxRows at editor_rows_allowed alone makes HyperFormula reject
|
||||
* any table bigger than that cap outright. See initSetup's own
|
||||
* Math.max(dataSource.length, editor_rows_allowed) fix.
|
||||
*
|
||||
* Handsontable 18.1 fixed the engine coupling upstream (#10672): the grid's
|
||||
* maxRows/maxCols no longer limit the HyperFormula engine, so a capped
|
||||
* maxRows no longer rejects a larger existing dataset. DC's own guard is
|
||||
* still in place; the first test below now pins the fixed behaviour.
|
||||
*/
|
||||
describe('maxRows must never be smaller than the actual loaded row count', () => {
|
||||
it('throws when maxRows is capped below the real row count (proves the mechanism)', () => {
|
||||
it('no longer throws when maxRows is capped below the real row count (HOT 18.1 #10672)', () => {
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
const colNames = ['PK', 'A', 'B']
|
||||
@@ -440,7 +445,7 @@ describe('maxRows must never be smaller than the actual loaded row count', () =>
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error?.message).toEqual('Sheet size limit exceeded')
|
||||
expect(error).toBeNull()
|
||||
})
|
||||
|
||||
it('does not throw when maxRows is never let below the actual row count (the fix)', () => {
|
||||
|
||||
@@ -287,9 +287,13 @@ describe('syncOverwrittenCommentForCell resolves the correct visual cell on a so
|
||||
* trigger it, independent of what it actually changes. editTable(),
|
||||
* cancelEdit(), and every other place editor.component.ts calls
|
||||
* updateSettings() after the grid could already be sorted (e.g. the user
|
||||
* sorted while still read-only, then clicked Edit) hits this. The fix
|
||||
* (updateSettingsSortSafe) clears the sort before the call and restores it
|
||||
* immediately afterward.
|
||||
* sorted while still read-only, then clicked Edit) hits this.
|
||||
*
|
||||
* Fixed upstream in Handsontable 18.1 (the formulas plugin now keeps its
|
||||
* index translations across updateSettings), so the corruption no longer
|
||||
* occurs. DC's own guard (updateSettingsSortSafe - clear the sort, call
|
||||
* updateSettings, restore the sort) is still in place and still correct;
|
||||
* this test now pins the fixed behaviour so a future regression shows up.
|
||||
*/
|
||||
describe('updateSettings must not run while a sort is active - it corrupts formula cell references', () => {
|
||||
const buildSortedGrid = (): Handsontable => {
|
||||
@@ -340,13 +344,13 @@ describe('updateSettings must not run while a sort is active - it corrupts formu
|
||||
hot.getDataAtRowProp(r, 'FORMULA_HARD_COL')
|
||||
)
|
||||
|
||||
it('demonstrates the failure mode: updateSettings while sorted corrupts formula cells', () => {
|
||||
it('no longer corrupts formula cells when updateSettings runs while sorted (HOT 18.1)', () => {
|
||||
const hot = buildSortedGrid()
|
||||
|
||||
hot.updateSettings({}, false)
|
||||
hot.render()
|
||||
|
||||
expect(formulaHardColValues(hot)).toContain('#REF!')
|
||||
expect(formulaHardColValues(hot)).not.toContain('#REF!')
|
||||
|
||||
hot.destroy()
|
||||
})
|
||||
|
||||
@@ -295,6 +295,40 @@ export class SasStoreService {
|
||||
this.columns.next(columnsData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Column metadata for the filter picker, read from the service.
|
||||
*
|
||||
* The picker's variable list normally comes from the columns that arrived
|
||||
* with the table (see setQueryVariablesFromTable). This is the fallback for
|
||||
* when there is no table - public/getcols reads the metadata only.
|
||||
*/
|
||||
public async getCols(libds: string) {
|
||||
const tables = { iwant: [{ libds: libds }] }
|
||||
|
||||
const res = await this.sasService.request('public/getcols', tables)
|
||||
|
||||
return res.adapterResponse.cols || []
|
||||
}
|
||||
|
||||
/**
|
||||
* Feeds the filter picker its variable list.
|
||||
*
|
||||
* `cols` is the column metadata that came back with the table, which costs
|
||||
* nothing extra and is the normal path. When the table was not returned - the
|
||||
* EDIT/VIEW row and cell limits abort the request before the data arrives -
|
||||
* that list is empty, which is what makes "please filter and try again"
|
||||
* unactionable: the picker has nothing to offer. Fall back to
|
||||
* public/getcols, which reads the column metadata without the rows.
|
||||
*/
|
||||
public async setQueryVariablesFromTable(libds: string, cols: any[]) {
|
||||
if (cols && cols.length > 0) {
|
||||
this.setQueryVariables(libds, cols)
|
||||
return
|
||||
}
|
||||
|
||||
this.setQueryVariables(libds, await this.getCols(libds))
|
||||
}
|
||||
|
||||
public async getChangeInfo(tableId: any) {
|
||||
let obj = { TABLE: tableId }
|
||||
let table = { SASControlTable: [obj] }
|
||||
|
||||
@@ -553,11 +553,27 @@ describe('DC Validator', () => {
|
||||
example_dqData
|
||||
)
|
||||
|
||||
// HOT 18.1 tightened `ColumnSettings['validator']` to the real
|
||||
// `string | RegExp | function` union (18.0 erased it behind an index
|
||||
// signature), so narrow it the way the production code does before
|
||||
// invoking it.
|
||||
const validatorOf = (rule: ReturnType<DcValidator['getRule']>) => {
|
||||
const validator = rule?.validator
|
||||
if (typeof validator !== 'function') {
|
||||
throw new Error('expected the rule to carry a function validator')
|
||||
}
|
||||
return validator as (
|
||||
this: any,
|
||||
value: any,
|
||||
callback: (valid: boolean) => void
|
||||
) => void
|
||||
}
|
||||
|
||||
it('exempts an invalid non-PK cell on a delete-marked row', () => {
|
||||
const dcValidator = buildValidator()
|
||||
const someNumRule = dcValidator.getRule('SOME_NUM')
|
||||
|
||||
someNumRule!.validator!.call(
|
||||
validatorOf(someNumRule).call(
|
||||
fakeCellProps(true),
|
||||
'not a number',
|
||||
(valid: boolean) => {
|
||||
@@ -570,7 +586,7 @@ describe('DC Validator', () => {
|
||||
const dcValidator = buildValidator()
|
||||
const someNumRule = dcValidator.getRule('SOME_NUM')
|
||||
|
||||
someNumRule!.validator!.call(
|
||||
validatorOf(someNumRule).call(
|
||||
fakeCellProps(true),
|
||||
null,
|
||||
(valid: boolean) => {
|
||||
@@ -583,7 +599,7 @@ describe('DC Validator', () => {
|
||||
const dcValidator = buildValidator()
|
||||
const someNumRule = dcValidator.getRule('SOME_NUM')
|
||||
|
||||
someNumRule!.validator!.call(
|
||||
validatorOf(someNumRule).call(
|
||||
fakeCellProps(false),
|
||||
'not a number',
|
||||
(valid: boolean) => {
|
||||
@@ -596,7 +612,7 @@ describe('DC Validator', () => {
|
||||
const dcValidator = buildValidator()
|
||||
const someNumRule = dcValidator.getRule('SOME_NUM')
|
||||
|
||||
someNumRule!.validator!.call(
|
||||
validatorOf(someNumRule).call(
|
||||
fakeCellProps(false),
|
||||
2,
|
||||
(valid: boolean) => {
|
||||
@@ -609,7 +625,7 @@ describe('DC Validator', () => {
|
||||
const dcValidator = buildValidator()
|
||||
const pkRule = dcValidator.getRule('PRIMARY_KEY_FIELD')
|
||||
|
||||
pkRule!.validator!.call(
|
||||
validatorOf(pkRule).call(
|
||||
fakeCellProps(true),
|
||||
'not a number',
|
||||
(valid: boolean) => {
|
||||
|
||||
@@ -1404,10 +1404,17 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.filter = true
|
||||
this.cdf.detectChanges()
|
||||
|
||||
this.sasStoreService.setQueryVariables(
|
||||
this.filterLibds,
|
||||
viewboxTable.hotTable.cols
|
||||
)
|
||||
// Table columns when we have them (the normal, cheapest source); when the
|
||||
// view was aborted by the row limit there is no loaded table to read them
|
||||
// from, so the store falls back to public/getcols.
|
||||
const cols =
|
||||
viewboxTable && viewboxTable.hotTable ? viewboxTable.hotTable.cols : []
|
||||
|
||||
this.sasStoreService
|
||||
.setQueryVariablesFromTable(this.filterLibds, cols)
|
||||
.catch((err: any) =>
|
||||
this.eventService.catchResponseError('public/getcols', err)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -286,6 +286,12 @@
|
||||
UPDATE
|
||||
</button>
|
||||
</div>
|
||||
<div class="admin-action">
|
||||
Export DC Library DDL
|
||||
<button (click)="openDdlExport()" class="btn btn-info btn-sm">
|
||||
EXPORT
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -306,3 +312,40 @@
|
||||
</button>
|
||||
</div>
|
||||
</clr-modal>
|
||||
|
||||
<clr-modal [(clrModalOpen)]="ddlModal">
|
||||
<h3 class="modal-title">Export DC Library DDL</h3>
|
||||
<div class="modal-body">
|
||||
<clr-select-container>
|
||||
<label>Flavour</label>
|
||||
<select [(ngModel)]="ddlFlavour" clrSelect>
|
||||
<option value="SAS">SAS</option>
|
||||
<option value="PGSQL">PGSQL</option>
|
||||
<option value="TSQL">TSQL</option>
|
||||
</select>
|
||||
</clr-select-container>
|
||||
@if (ddlFlavour !== 'SAS') {
|
||||
<clr-input-container>
|
||||
<label>Schema (optional)</label>
|
||||
<input
|
||||
clrInput
|
||||
type="text"
|
||||
[(ngModel)]="ddlSchema"
|
||||
placeholder="e.g. DC"
|
||||
/>
|
||||
</clr-input-container>
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline"
|
||||
(click)="ddlModal = false"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" (click)="exportDdl()">
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
</clr-modal>
|
||||
@@ -42,6 +42,9 @@ export class SystemComponent implements OnInit {
|
||||
Infinity = Infinity
|
||||
dcLib: string = globals.dcLib
|
||||
targetLibref: string = globals.dcLib
|
||||
ddlModal: boolean = false
|
||||
ddlFlavour: string = 'SAS'
|
||||
ddlSchema: string = ''
|
||||
|
||||
licenceState = this.licenceService.licenceState
|
||||
settings: AppSettings
|
||||
@@ -92,6 +95,32 @@ export class SystemComponent implements OnInit {
|
||||
window.open(downUrl)
|
||||
}
|
||||
|
||||
openDdlExport() {
|
||||
this.ddlFlavour = 'SAS'
|
||||
this.ddlSchema = ''
|
||||
this.ddlModal = true
|
||||
}
|
||||
|
||||
exportDdl() {
|
||||
let sasjsConfig = this.sasService.getSasjsConfig()
|
||||
let storage = sasjsConfig.serverUrl
|
||||
let metaData = sasjsConfig.appLoc
|
||||
let path = this.sasService.getExecutionPath()
|
||||
let downUrl =
|
||||
storage +
|
||||
path +
|
||||
'/?_program=' +
|
||||
metaData +
|
||||
'/services/admin/exportdb&flavour=' +
|
||||
encodeURIComponent(this.ddlFlavour)
|
||||
let schema = this.ddlSchema.trim()
|
||||
if (this.ddlFlavour !== 'SAS' && schema) {
|
||||
downUrl += '&schema=' + encodeURIComponent(schema)
|
||||
}
|
||||
window.open(downUrl)
|
||||
this.ddlModal = false
|
||||
}
|
||||
|
||||
refreshDataCatalog() {
|
||||
this.refreshingDataCatalog = true
|
||||
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
const nodePath = require('path')
|
||||
|
||||
let appLoc = nodePath.join(..._program.split('services')[0].split('/'))
|
||||
const sasjsRoot = nodePath.resolve(weboutPath, '..', '..', '..')
|
||||
const driveRoot = nodePath.resolve(sasjsRoot, 'drive')
|
||||
|
||||
eval(fs.readFileSync(nodePath.resolve(driveRoot, 'files', appLoc, 'services', 'dcMockUtils.js'), 'utf8'))
|
||||
|
||||
/**
|
||||
* Mock of services/admin/exportdb.sas - exports the DC library as DDL.
|
||||
*
|
||||
* The real service streams mp_ds2ddl output for every table in &DC_LIBREF, in
|
||||
* the requested flavour, and mp_lib2inserts appends the rows for the SAS and
|
||||
* PGSQL flavours. This mock emits the DDL only - a basic, readable rendering
|
||||
* per flavour, enough to exercise the System screen's Export DC Library DDL
|
||||
* action end to end (the download headers, the filename, the flavour and the
|
||||
* optional schema). It deliberately does not append INSERTs: the real service
|
||||
* does, but for a mock that would mean serialising the whole library's rows
|
||||
* (MPE_X_WIDE alone is 200,000 cells) for no extra test value.
|
||||
*
|
||||
* Params arrive from the URL the client opens:
|
||||
* <serverUrl>/SASjsApi/stp/execute/?_program=<appLoc>/services/admin/exportdb
|
||||
* &flavour=SAS|PGSQL|TSQL[&schema=<name>]
|
||||
* The schema box only applies to the DB flavours, and the client omits the
|
||||
* param entirely when it is blank, so both must be read defensively - an
|
||||
* unset URL param is an undeclared const here, not an empty string.
|
||||
*/
|
||||
|
||||
const ddlFlavour = (typeof flavour === 'undefined' || !flavour)
|
||||
? 'SAS'
|
||||
: String(flavour).toUpperCase()
|
||||
|
||||
const dcConf = loadDcSettings(driveRoot, appLoc)
|
||||
const libref = dcConf.dcLibref
|
||||
const dataDir = dcConf.dataDir
|
||||
|
||||
// if no schema param, the DC libref is used (as in exportdb.sas)
|
||||
const ddlSchema = (typeof schema === 'undefined' || !schema)
|
||||
? libref
|
||||
: String(schema)
|
||||
|
||||
// ─── Column types, per flavour ───────────────────────────────────────────────
|
||||
|
||||
function colType(col, flavour) {
|
||||
const isChar = String(col.type).toUpperCase() === 'C'
|
||||
const fmt = String(col.format || '').toUpperCase()
|
||||
const len = Number(col.length) || (isChar ? 1 : 8)
|
||||
|
||||
if (flavour === 'SAS') {
|
||||
return isChar ? `char(${len})` : 'num'
|
||||
}
|
||||
|
||||
// PGSQL and TSQL share the mapping; only the quoting differs. The one type
|
||||
// that must not be shared is the datetime: T-SQL's TIMESTAMP is a deprecated
|
||||
// synonym for rowversion - a binary row version, not a datetime - so it maps
|
||||
// to DATETIME2 there. DOUBLE PRECISION is a T-SQL synonym for float, so it
|
||||
// stays valid on both.
|
||||
if (isChar) return `VARCHAR(${len})`
|
||||
if (fmt.startsWith('DATETIME'))
|
||||
return flavour === 'TSQL' ? 'DATETIME2' : 'TIMESTAMP'
|
||||
if (fmt.startsWith('DATE')) return 'DATE'
|
||||
if (fmt.startsWith('TIME')) return 'TIME'
|
||||
return 'DOUBLE PRECISION'
|
||||
}
|
||||
|
||||
function colRef(name, flavour) {
|
||||
if (flavour === 'PGSQL') return `"${name}"`
|
||||
if (flavour === 'TSQL') return `[${name}]`
|
||||
return name
|
||||
}
|
||||
|
||||
function tableRef(table, flavour) {
|
||||
if (flavour === 'TSQL') return `[${ddlSchema}].[${table}]`
|
||||
return `${ddlSchema}.${table}`
|
||||
}
|
||||
|
||||
// ─── Buskeys (for the unique index), from MPE_TABLES ─────────────────────────
|
||||
|
||||
const buskeys = {}
|
||||
try {
|
||||
const reg = makeTableLoader(dataDir)('MPE_TABLES')
|
||||
for (const row of ((reg && reg.rows) || [])) {
|
||||
const dsn = String(colVal(row, 'DSN') || '').trim().toUpperCase()
|
||||
const buskey = String(colVal(row, 'BUSKEY') || '').trim()
|
||||
if (dsn && buskey) buskeys[dsn] = buskey
|
||||
}
|
||||
} catch(err) { /* no MPE_TABLES - the DDL is still emitted, without indexes */ }
|
||||
|
||||
// ─── Build the DDL ───────────────────────────────────────────────────────────
|
||||
|
||||
const lines = []
|
||||
const stamp = new Date().toISOString().replace('T', ':').substring(0, 19)
|
||||
lines.push(`/* DDL generated by the Data Controller mock on ${stamp} */`)
|
||||
|
||||
if (ddlFlavour === 'PGSQL') {
|
||||
lines.push(`CREATE SCHEMA ${ddlSchema};`)
|
||||
}
|
||||
|
||||
let tables = []
|
||||
try {
|
||||
tables = fs.readdirSync(dataDir)
|
||||
.filter(f => f.endsWith('.json'))
|
||||
.map(f => {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(nodePath.resolve(dataDir, f), {encoding:'utf8'}).toString())
|
||||
} catch(e) { return null }
|
||||
})
|
||||
.filter(t => t && t.name && Array.isArray(t.columns))
|
||||
.sort((a, b) => String(a.name).localeCompare(String(b.name)))
|
||||
} catch(err) { /* nothing to export */ }
|
||||
|
||||
for (const table of tables) {
|
||||
const tableName = String(table.name).toUpperCase()
|
||||
const cols = table.columns.filter(c => String(c.name || '').trim() !== '')
|
||||
if (cols.length === 0) continue
|
||||
|
||||
if (ddlFlavour === 'SAS') {
|
||||
lines.push(`/* SAS flavour DDL for ${libref}.${tableName} */`)
|
||||
lines.push(`create table ${tableRef(tableName, ddlFlavour)}(`)
|
||||
} else if (ddlFlavour === 'PGSQL') {
|
||||
lines.push(`/* Postgres Flavour DDL for ${libref}.${tableName} */`)
|
||||
lines.push(`CREATE TABLE ${tableRef(tableName, ddlFlavour)} (`)
|
||||
} else {
|
||||
lines.push(`/* TSQL Flavour DDL for ${libref}.${tableName} */`)
|
||||
lines.push(`create table ${tableRef(tableName, ddlFlavour)}(`)
|
||||
}
|
||||
|
||||
cols.forEach((col, idx) => {
|
||||
const name = colRef(String(col.name).toUpperCase(), ddlFlavour)
|
||||
const prefix = idx === 0 ? ' ' : ' ,'
|
||||
lines.push(`${prefix}${name} ${colType(col, ddlFlavour)}`)
|
||||
})
|
||||
lines.push(');')
|
||||
|
||||
// a unique index on the buskey, where MPE_TABLES declares one - SAS has no
|
||||
// equivalent in mp_ds2ddl output, so it is skipped for that flavour
|
||||
const buskey = buskeys[tableName]
|
||||
if (buskey && ddlFlavour !== 'SAS') {
|
||||
const keys = buskey.split(/[\s,]+/).filter(Boolean)
|
||||
.map(k => colRef(k.toUpperCase(), ddlFlavour))
|
||||
lines.push(
|
||||
`CREATE UNIQUE INDEX ${colRef(tableName + '_pk', ddlFlavour)} ON ` +
|
||||
`${tableRef(tableName, ddlFlavour)}(\n ${keys.join('\n ,')}\n);`
|
||||
)
|
||||
}
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
// ─── Stream it back as a download, as mp_streamfile does ─────────────────────
|
||||
|
||||
const filename = `${libref}.ddl`
|
||||
fs.writeFile(
|
||||
_SASJS_WEBOUT_HEADERS,
|
||||
`Content-type: text/plain \n Content-Disposition: attachment;filename="${filename}"`,
|
||||
function (err) {
|
||||
if (err) throw err
|
||||
}
|
||||
)
|
||||
|
||||
_webout = lines.join('\n') + '\n'
|
||||
@@ -76,9 +76,32 @@ const tables = {
|
||||
tx_to: 253717919999,
|
||||
var_scope: 'DC',
|
||||
var_name: 'DC_MAXOBS_WEBEDIT',
|
||||
var_value: '100',
|
||||
var_value: '250',
|
||||
var_active: 1,
|
||||
var_desc: 'Maximum observations for editing in the EDIT screen.'
|
||||
var_desc:
|
||||
'Maximum observations for editing in the EDIT screen. The ' +
|
||||
'DC_MAXCELLS_WEBEDIT limit applies at the same time, whichever is ' +
|
||||
'reached first.'
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
tx_to: 253717919999,
|
||||
var_scope: 'DC',
|
||||
var_name: 'DC_MAXCELLS_WEBEDIT',
|
||||
var_value: '200000',
|
||||
var_active: 1,
|
||||
var_desc:
|
||||
'Maximum CELLS (rows x columns) for editing in the EDIT screen - a ' +
|
||||
'wide table is expensive long before it is tall.'
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
tx_to: 253717919999,
|
||||
var_scope: 'DC',
|
||||
var_name: 'DC_MAXOBS_WEBVIEW',
|
||||
var_value: '2000',
|
||||
var_active: 1,
|
||||
var_desc: 'Maximum observations for the VIEW screen.'
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
@@ -2201,6 +2224,37 @@ tables.MPE_TABLES.push({
|
||||
notes: 'Full table search demo table'
|
||||
})
|
||||
|
||||
// ─── MPE_X_WIDE: wide table for the EDIT cell limit ─────────────────────────
|
||||
// 200 rows x 1001 columns (ROW_ID + 500 single-character + 500 numeric) is
|
||||
// 200,400 cells - over DC_MAXCELLS_WEBEDIT (200000) while staying under
|
||||
// DC_MAXOBS_WEBEDIT (250), so it exercises the CELL limit on its own and the
|
||||
// row limit spec can use MPE_X_SEARCH (1000 rows) instead. Values are one
|
||||
// character / one digit on purpose: the point is width, and small values keep
|
||||
// the seeded JSON near 1MB rather than the 25MB the real WIDEBOY fixture (20
|
||||
// character fields) would take.
|
||||
tables.MPE_X_WIDE = []
|
||||
for (let n = 0; n < 200; n++) {
|
||||
const wideRow = { ROW_ID: n + 1 }
|
||||
for (let c = 1; c <= 500; c++) {
|
||||
wideRow['CHAR' + c] = String.fromCharCode(65 + ((n + c) % 26))
|
||||
}
|
||||
for (let c = 1; c <= 500; c++) {
|
||||
wideRow['NUM' + c] = (n + c) % 10
|
||||
}
|
||||
tables.MPE_X_WIDE.push(wideRow)
|
||||
}
|
||||
|
||||
tables.MPE_TABLES.push({
|
||||
tx_from: 0,
|
||||
tx_to: 127490111999,
|
||||
libref: dcLibref,
|
||||
dsn: 'MPE_X_WIDE',
|
||||
num_of_approvals_required: 1,
|
||||
loadtype: 'UPDATE',
|
||||
buskey: 'ROW_ID CHAR1 NUM1',
|
||||
notes: 'Wide table fixture for the EDIT cell limit'
|
||||
})
|
||||
|
||||
// Column metadata for each table, extracted from mpe_makedatamodel.sas
|
||||
// Each entry: { name, type (N/C), length, format, label, notnull (bool) }
|
||||
const schema = {
|
||||
@@ -4976,6 +5030,31 @@ const schema = {
|
||||
MPE_MAXKEYVALUES: []
|
||||
}
|
||||
|
||||
// Columns for MPE_X_WIDE - built rather than spelled out, there are 1001.
|
||||
schema.MPE_X_WIDE = [
|
||||
{ name: 'ROW_ID', type: 'N', length: 8, format: '', label: '', notnull: true }
|
||||
]
|
||||
for (let c = 1; c <= 500; c++) {
|
||||
schema.MPE_X_WIDE.push({
|
||||
name: 'CHAR' + c,
|
||||
type: 'C',
|
||||
length: 1,
|
||||
format: '',
|
||||
label: '',
|
||||
notnull: false
|
||||
})
|
||||
}
|
||||
for (let c = 1; c <= 500; c++) {
|
||||
schema.MPE_X_WIDE.push({
|
||||
name: 'NUM' + c,
|
||||
type: 'N',
|
||||
length: 8,
|
||||
format: '',
|
||||
label: '',
|
||||
notnull: false
|
||||
})
|
||||
}
|
||||
|
||||
// ─── TESTDATA library: demo user tables for the Cypress E2E suite ───────────
|
||||
// These tables hold the DQ-rule demo columns (HARDSELECT, READONLY, HIDDEN,
|
||||
// ROUND, NUMBER_FORMAT, HARDREGEX, SOFTREGEX) and the formula demo columns
|
||||
|
||||
@@ -148,14 +148,63 @@ function _parseCsvLine(line) {
|
||||
// ─── SAS format <-> ISO conversions ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Derives a DDTYPE (DATE, DATETIME, TIME, N, C) from a SAS column format string.
|
||||
* Column metadata, mirroring %mp_getcols.sas.
|
||||
*
|
||||
* The SAS macro runs proc contents and derives, per column: NAME (upcased),
|
||||
* TYPE (C/N), LENGTH, LABEL (defaulting to the name), VARNUM, FMTNAME, FORMAT
|
||||
* and DDTYPE - CHARACTER, NUMERIC, DATETIME, DATE or TIME. The date list is
|
||||
* the macro's (YYMMDD, MMDDYY, DDMMYY, MONYY, E8601DA, B8601DA, E8601DT,
|
||||
* NLDATM, NLDATE), so a column types the same way here as it does in the
|
||||
* service that shares this code.
|
||||
*
|
||||
* @param {Array} schemaCols the table's columns as the mock data stores them
|
||||
* ({name, type, length, format, label}), the equivalent of the proc contents
|
||||
* output the macro reads
|
||||
* @returns {Array} one entry per column, in the macro's shape and order
|
||||
*/
|
||||
function mpGetcols(schemaCols) {
|
||||
return (schemaCols || []).map((col, index) => {
|
||||
const name = String(col.name || '').toUpperCase()
|
||||
const isChar = String(col.type || '').toUpperCase() === 'C'
|
||||
const fmtname = String(col.format || '')
|
||||
.replace(/[\d.]+$/, '')
|
||||
.toUpperCase()
|
||||
|
||||
let ddtype
|
||||
if (isChar) ddtype = 'CHARACTER'
|
||||
else if (fmtname.startsWith('DATETIME') || fmtname.startsWith('E8601DT')
|
||||
|| fmtname.startsWith('NLDATM')) ddtype = 'DATETIME'
|
||||
else if (fmtname.startsWith('DATE') || fmtname.startsWith('DDMMYY')
|
||||
|| fmtname.startsWith('MMDDYY') || fmtname.startsWith('YYMMDD')
|
||||
|| fmtname.startsWith('E8601DA') || fmtname.startsWith('B8601DA')
|
||||
|| fmtname.startsWith('MONYY') || fmtname.startsWith('NLDATE')) ddtype = 'DATE'
|
||||
else if (fmtname.startsWith('TIME')) ddtype = 'TIME'
|
||||
else ddtype = 'NUMERIC'
|
||||
|
||||
return {
|
||||
NAME: name,
|
||||
TYPE: isChar ? 'C' : 'N',
|
||||
LENGTH: Number(col.length) || 0,
|
||||
LABEL: col.label || col.name,
|
||||
VARNUM: index + 1,
|
||||
FORMAT: String(col.format || ''),
|
||||
FMTNAME: fmtname,
|
||||
DDTYPE: ddtype
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives a DDTYPE (DATE, DATETIME, TIME, N, C) from a SAS column format
|
||||
* string, in the shape the client expects. Delegates to mpGetcols so the
|
||||
* inference is the macro's, rather than a second format list that can drift
|
||||
* away from the service's.
|
||||
*/
|
||||
function getDdType(col) {
|
||||
const fmt = (col.format || '').toLowerCase()
|
||||
if (fmt.startsWith('datetime') || fmt.startsWith('e8601dt')) return 'DATETIME'
|
||||
if (fmt.startsWith('date') || fmt.startsWith('yymmdd') || fmt.startsWith('ddmmyy') || fmt.startsWith('mmddyy')) return 'DATE'
|
||||
if (fmt.startsWith('time')) return 'TIME'
|
||||
return col.type === 'N' ? 'N' : 'C'
|
||||
const ddtype = mpGetcols([col])[0].DDTYPE
|
||||
if (ddtype === 'CHARACTER') return 'C'
|
||||
if (ddtype === 'NUMERIC') return 'N'
|
||||
return ddtype
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1102,6 +1151,61 @@ function loadDcSettings(driveRoot, appLoc) {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the DC-scope options out of MPE_CONFIG, mirroring the loop in the real
|
||||
* backend's generated settings.sas: only rows that are active (var_active=1)
|
||||
* and current (now < tx_to) count, and the latest tx_from wins. Returns a map
|
||||
* of var_name -> trimmed var_value.
|
||||
*
|
||||
* An option that is absent, or switched off, is simply missing from the map so
|
||||
* the caller's own default applies - which is what the real service's
|
||||
* %symexist fallback does (a switched-off option means "use the code default",
|
||||
* not "no limit").
|
||||
*
|
||||
* Requires nodePath, fs and the caller's driveRoot/appLoc/dataDir.
|
||||
*/
|
||||
function loadDcConfig(driveRoot, appLoc, dataDir) {
|
||||
const out = {}
|
||||
try {
|
||||
const configFile = nodePath.resolve(
|
||||
dataDir || nodePath.resolve(driveRoot, 'files', appLoc, 'data', 'DC_JSLIB'),
|
||||
'mpe_config.json'
|
||||
)
|
||||
const config = JSON.parse(fs.readFileSync(configFile, {encoding:'utf8'}).toString())
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const latest = {}
|
||||
for (const row of (config.rows || [])) {
|
||||
// colVal, not row.var_name: a row that has been through the app's
|
||||
// own edit and approve flow comes back from the adapter with
|
||||
// upper-case keys, and a direct property read would skip it - which
|
||||
// silently drops the option back to its default.
|
||||
if (colVal(row, 'VAR_SCOPE') !== 'DC') continue
|
||||
if (Number(colVal(row, 'VAR_ACTIVE')) !== 1) continue
|
||||
if (Number(colVal(row, 'TX_TO')) <= now) continue
|
||||
const name = String(colVal(row, 'VAR_NAME') || '').trim()
|
||||
if (!name) continue
|
||||
const from = Number(colVal(row, 'TX_FROM')) || 0
|
||||
if (latest[name] === undefined || from > latest[name]) {
|
||||
const val = colVal(row, 'VAR_VALUE')
|
||||
latest[name] = from
|
||||
out[name] = String(val == null ? '' : val).trim()
|
||||
}
|
||||
}
|
||||
} catch(err) { /* no MPE_CONFIG - every option falls back to its default */ }
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* A numeric DC option out of a loadDcConfig() map, or the service's own default
|
||||
* when the option is absent, blank, or not a number.
|
||||
*/
|
||||
function dcConfigNumber(cfg, name, fallback) {
|
||||
const raw = cfg ? cfg[name] : undefined
|
||||
if (raw === undefined || raw === '') return fallback
|
||||
const num = Number(raw)
|
||||
return isNaN(num) ? fallback : num
|
||||
}
|
||||
|
||||
// ─── %webout(OPEN/OBJ/CLOSE) emulation ───────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -191,6 +191,9 @@ if (tableData && tableData.rows && tableData.columns) {
|
||||
// ─── Build response ──────────────────────────────────────────────────────────
|
||||
|
||||
let response
|
||||
// Set when the row or cell limit is exceeded - mirrors the %mp_abort in
|
||||
// getdata.sas, and is emitted as sasjsAbort below.
|
||||
let abortMsg = ''
|
||||
|
||||
if (tableData && tableData.columns && tableData.rows) {
|
||||
// Exclude temporal columns (TX_FROM/TX_TO etc.) from the editor view
|
||||
@@ -232,6 +235,39 @@ if (tableData && tableData.columns && tableData.rows) {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Row and cell limits ─────────────────────────────────────────────────
|
||||
// Mirrors getdata.sas: DC_MAXOBS_WEBEDIT rows, or DC_MAXCELLS_WEBEDIT
|
||||
// cells (rows x columns), whichever is reached first. Both are read from
|
||||
// MPE_CONFIG, the way the real service picks them up from the settings
|
||||
// program it generates - so editing the option in DC changes what the mock
|
||||
// allows, and an option that is absent (or switched off, var_active=0)
|
||||
// falls back to the shipped default. The check runs after the
|
||||
// PRE_EDIT_HOOK, as the single check in getdata.sas does - a hook may
|
||||
// replace the data with a larger table. The cell count includes the
|
||||
// housekeeping delete column, which is what getdata.sas counts.
|
||||
// MPE_CONFIG lives in the control library, not in the table's own library,
|
||||
// so this reads mpeDataDir - the same directory the MPE_TABLES lookup above
|
||||
// uses. Passing the requested table's dataDir made the read fail for every
|
||||
// non-control libref, and the catch then fell back to the shipped defaults.
|
||||
const dcCfg = loadDcConfig(driveRoot, appLoc, mpeDataDir)
|
||||
const maxObsWebEdit = dcConfigNumber(dcCfg, 'DC_MAXOBS_WEBEDIT', 250)
|
||||
const maxCellsWebEdit = dcConfigNumber(dcCfg, 'DC_MAXCELLS_WEBEDIT', 200000)
|
||||
const editColCount = visibleColumns.length + 1
|
||||
const cellCount = visibleRows.length * editColCount
|
||||
if (visibleRows.length > maxObsWebEdit) {
|
||||
abortMsg =
|
||||
'Table is too big (' +
|
||||
visibleRows.length +
|
||||
' rows) - please filter and try again!'
|
||||
} else if (cellCount > maxCellsWebEdit) {
|
||||
abortMsg =
|
||||
'Selection is too wide (' +
|
||||
visibleRows.length +
|
||||
' rows x ' +
|
||||
editColCount +
|
||||
' cols) - please filter and try again!'
|
||||
}
|
||||
|
||||
// Augment rows: add delete flag, normalise keys, convert temporal values to ISO
|
||||
const colLookup = {}
|
||||
for (const col of visibleColumns) {
|
||||
@@ -264,22 +300,25 @@ if (tableData && tableData.columns && tableData.rows) {
|
||||
else if (ddtype === 'TIME') tmVars.push(col.name)
|
||||
}
|
||||
|
||||
// Build cols array
|
||||
const cols = visibleColumns.map(col => {
|
||||
const ddtype = getDdType(col)
|
||||
const fmtname = col.format
|
||||
? col.format.replace(/[\d.]+$/, '').replace('datetime', 'DATETIME').replace('date', 'DATE').replace('time', 'TIME').replace('best', 'BEST').replace('E8601DT', 'DATETIME')
|
||||
: ' '
|
||||
// Build cols array. Every column of the table, as getdata.sas emits them:
|
||||
// the temporal columns are dropped from the data (the rows below) but not
|
||||
// from the cols payload, so a filter can still be built on them. mpGetcols
|
||||
// is the JS counterpart of %mp_getcols.sas - the macro getdata.sas itself
|
||||
// calls to type these columns - so the editor sees the same DDTYPE the
|
||||
// service would give it.
|
||||
const cols = mpGetcols(tableData.columns).map((c, i) => {
|
||||
const raw = tableData.columns[i]
|
||||
const ddtype = c.DDTYPE === 'CHARACTER' ? 'C' : c.DDTYPE === 'NUMERIC' ? 'N' : c.DDTYPE
|
||||
let coltype
|
||||
if (ddtype === 'DATE') coltype = `{"data":"${col.name}","type":"date"}`
|
||||
else if (ddtype === 'DATETIME') coltype = `{"data":"${col.name}","type":"datetime"}`
|
||||
else if (ddtype === 'TIME') coltype = `{"data":"${col.name}","type":"time"}`
|
||||
else if (ddtype === 'N') coltype = `{"data":"${col.name}","type":"numeric","format":"0"}`
|
||||
else coltype = `{"data":"${col.name}"}`
|
||||
if (ddtype === 'DATE') coltype = `{"data":"${raw.name}","type":"date"}`
|
||||
else if (ddtype === 'DATETIME') coltype = `{"data":"${raw.name}","type":"datetime"}`
|
||||
else if (ddtype === 'TIME') coltype = `{"data":"${raw.name}","type":"time"}`
|
||||
else if (ddtype === 'N') coltype = `{"data":"${raw.name}","type":"numeric","format":"0"}`
|
||||
else coltype = `{"data":"${raw.name}"}`
|
||||
return {
|
||||
NAME: col.name,
|
||||
LABEL: col.label || col.name,
|
||||
FMTNAME: fmtname || ' ',
|
||||
NAME: c.NAME,
|
||||
LABEL: c.LABEL,
|
||||
FMTNAME: c.FMTNAME || ' ',
|
||||
DDTYPE: ddtype,
|
||||
CLS_RULE: 'READ',
|
||||
MEMLABEL: ' ',
|
||||
@@ -354,15 +393,20 @@ if (tableData && tableData.columns && tableData.rows) {
|
||||
}
|
||||
|
||||
webOutOpen()
|
||||
webOutObj(response.approvers, 'approvers')
|
||||
webOutObj(response.cols, 'cols')
|
||||
webOutObj(response.dqdata, 'dqdata')
|
||||
webOutObj(response.dqrules, 'dqrules')
|
||||
webOutObj(response.dsmeta, 'dsmeta')
|
||||
webOutObj(response.maxvarlengths, 'maxvarlengths')
|
||||
webOutObj(response.query, 'query')
|
||||
webOutObj(response.sasdata, 'sasdata', response.$sasdata)
|
||||
webOutObj(response.sasparams, 'sasparams')
|
||||
webOutObj(response.versions, 'versions')
|
||||
webOutObj(response.xl_rules, 'xl_rules')
|
||||
if (abortMsg) {
|
||||
webOutObj([{ MSG: abortMsg, MAC: 'getdata' }], 'sasjsAbort')
|
||||
webOutObj([{ STATUS: 'ERROR' }], 'sasparams')
|
||||
} else {
|
||||
webOutObj(response.approvers, 'approvers')
|
||||
webOutObj(response.cols, 'cols')
|
||||
webOutObj(response.dqdata, 'dqdata')
|
||||
webOutObj(response.dqrules, 'dqrules')
|
||||
webOutObj(response.dsmeta, 'dsmeta')
|
||||
webOutObj(response.maxvarlengths, 'maxvarlengths')
|
||||
webOutObj(response.query, 'query')
|
||||
webOutObj(response.sasdata, 'sasdata', response.$sasdata)
|
||||
webOutObj(response.sasparams, 'sasparams')
|
||||
webOutObj(response.versions, 'versions')
|
||||
webOutObj(response.xl_rules, 'xl_rules')
|
||||
}
|
||||
webOutClose()
|
||||
@@ -0,0 +1,79 @@
|
||||
const nodePath = require('path')
|
||||
|
||||
let appLoc = nodePath.join(..._program.split('services')[0].split('/'))
|
||||
const sasjsRoot = nodePath.resolve(weboutPath, '..', '..', '..')
|
||||
const driveRoot = nodePath.resolve(sasjsRoot, 'drive')
|
||||
const dcLibref = 'DC_JSLIB'
|
||||
|
||||
// Load shared DC mock utilities
|
||||
eval(fs.readFileSync(nodePath.resolve(driveRoot, 'files', appLoc, 'services', 'dcMockUtils.js'), 'utf8'))
|
||||
|
||||
// ─── Parse input ──────────────────────────────────────────────────────────────
|
||||
// getcols.sas reads %mf_getvalue(work.iwant,libds). The adapter serialises the
|
||||
// input table as CSV, which fetchTable parses - so the row is `_iwantRow` (the
|
||||
// runtime already owns the name `iwant`).
|
||||
|
||||
const _iwantRow = fetchTable('iwant')[0] || {}
|
||||
const libds = String(_iwantRow.libds || _iwantRow.LIBDS || '').trim()
|
||||
|
||||
let libref = dcLibref
|
||||
let table = ''
|
||||
if (libds) {
|
||||
const parts = libds.split('.')
|
||||
if (parts.length >= 2) {
|
||||
libref = parts[0].trim()
|
||||
table = parts[1].trim()
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Load the table schema ────────────────────────────────────────────────────
|
||||
// Same resolution as getdata.js: the requested libref first, then any libref
|
||||
// (a LIBDS that doesn't match the data folder still resolves).
|
||||
|
||||
let tableData = table ? makeTableLoader(libDataDir(libref))(table) : null
|
||||
if (!tableData) {
|
||||
// Fall back to searching every libref if the table isn't in the requested
|
||||
// libref (e.g. a LIBDS that doesn't match the actual data folder), and take
|
||||
// the libref that actually held it for the registry lookup below.
|
||||
const found = loadTableAnyLib(table)
|
||||
if (found) {
|
||||
tableData = found.data
|
||||
libref = found.libref
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Build the column list ────────────────────────────────────────────────────
|
||||
// Every column, as getdata.sas emits them: the temporal columns are dropped from
|
||||
// the data, not from the cols payload, so a filter can still be built on them -
|
||||
// which is exactly what "please filter and try again" wants to do.
|
||||
|
||||
let cols = []
|
||||
|
||||
if (tableData && tableData.columns) {
|
||||
// mpGetcols, then the mapping getcols.sas applies on top of %mp_getcols:
|
||||
// DDTYPE in the client's vocabulary, and TYPE too - on the normal path
|
||||
// mergeColsRules() sets TYPE from the loaded table's formats, and the picker
|
||||
// keys its operator set and its value quoting on it. This list exists
|
||||
// because that load did not happen, so it has to carry it.
|
||||
cols = mpGetcols(tableData.columns)
|
||||
.map(c => {
|
||||
const ddtype = c.DDTYPE === 'CHARACTER' ? 'C' : c.DDTYPE === 'NUMERIC' ? 'N' : c.DDTYPE
|
||||
return {
|
||||
NAME: c.NAME,
|
||||
LABEL: c.LABEL,
|
||||
FMTNAME: c.FMTNAME || ' ',
|
||||
DDTYPE: ddtype,
|
||||
TYPE: ['DATE', 'DATETIME', 'TIME', 'N'].includes(ddtype) ? 'num' : 'char',
|
||||
CLS_RULE: 'READ',
|
||||
MEMLABEL: ' ',
|
||||
DESC: ' ',
|
||||
LONGDESC: ' ',
|
||||
COLTYPE: ' '
|
||||
}
|
||||
})
|
||||
.sort((a, b) => a.NAME.localeCompare(b.NAME))
|
||||
}
|
||||
|
||||
webOutOpen()
|
||||
webOutObj(cols, 'cols')
|
||||
webOutClose()
|
||||
@@ -8,9 +8,15 @@ const mpeDataDir = nodePath.resolve(driveRoot, 'files', appLoc, 'data', dcLibref
|
||||
|
||||
eval(fs.readFileSync(nodePath.resolve(driveRoot, 'files', appLoc, 'services', 'dcMockUtils.js'), 'utf8'))
|
||||
|
||||
// Row cap for the viewer - mirrors DC_MAXOBS_WEBVIEW in viewdata.sas, and the
|
||||
// Row cap for the viewer - mirrors DC_MAXOBS_WEBVIEW in viewdata.sas, read from
|
||||
// MPE_CONFIG the way the real service reads it from the settings program (an
|
||||
// absent or switched-off option falls back to the shipped default). Also the
|
||||
// MAXROWS value reported in sasparams.
|
||||
const maxObsWebView = 500
|
||||
const maxObsWebView = dcConfigNumber(
|
||||
loadDcConfig(driveRoot, appLoc, mpeDataDir),
|
||||
'DC_MAXOBS_WEBVIEW',
|
||||
2000
|
||||
)
|
||||
|
||||
// ─── Parse input ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -95,16 +101,15 @@ let vars = {}
|
||||
let nobs = 0
|
||||
|
||||
if (tableData && tableData.columns) {
|
||||
cols = tableData.columns.map(col => {
|
||||
const ddtype = getDdType(col)
|
||||
const fmtname = col.format
|
||||
? col.format.replace(/[\d.]+$/, '').replace('datetime', 'DATETIME').replace('date', 'DATE').replace('time', 'TIME').replace('best', 'BEST').replace('E8601DT', 'DATETIME')
|
||||
: ' '
|
||||
const formatStr = col.type === 'N' ? (col.format || '8.') : ('$' + col.length + '.')
|
||||
// mpGetcols, the JS counterpart of %mp_getcols.sas - the macro viewdata.sas
|
||||
// calls for its own column list.
|
||||
cols = mpGetcols(tableData.columns).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 {
|
||||
NAME: col.name, TYPE: col.type === 'N' ? 'N' : 'C', LENGTH: col.length,
|
||||
FORMAT: formatStr, FMTNAME: fmtname || ' ', DDTYPE: ddtype,
|
||||
LABEL: col.label || col.name
|
||||
NAME: c.NAME, TYPE: c.TYPE, LENGTH: c.LENGTH,
|
||||
FORMAT: formatStr, FMTNAME: c.FMTNAME || ' ', DDTYPE: ddtype,
|
||||
LABEL: c.LABEL
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Generated
+201
-400
@@ -6,7 +6,7 @@
|
||||
"": {
|
||||
"name": "dc-sas",
|
||||
"dependencies": {
|
||||
"@sasjs/cli": "4.20.4",
|
||||
"@sasjs/cli": "4.20.5",
|
||||
"@sasjs/core": "5.2.8"
|
||||
}
|
||||
},
|
||||
@@ -153,49 +153,14 @@
|
||||
"lodash.isnil": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "2.0.5",
|
||||
"run-parallel": "^1.1.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.stat": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.walk": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.scandir": "2.1.5",
|
||||
"fastq": "^1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/adapter": {
|
||||
"version": "4.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-4.19.0.tgz",
|
||||
"integrity": "sha512-AhSldduDFPAuhRsXdzeiwMqEm0lm6dnh2yVxDSUmxxw8V9YShmeGPmEzvU8n2Cl4VAyrhc+Ietc5YWFYC03G+Q==",
|
||||
"version": "4.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-4.19.1.tgz",
|
||||
"integrity": "sha512-IpN6UPSdqVb5cAWEBt+jG/i0rhgCubdyp5kxdouuX3ifL313oJW2ZPVUNUvs0+ZGzsXWcGDTNoEzzTrGKiqJlQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@sasjs/utils": "^3.6.0",
|
||||
"axios": "1.18.1",
|
||||
"axios": "1.20.0",
|
||||
"axios-cookiejar-support": "5.0.5",
|
||||
"form-data": "4.0.6",
|
||||
"https": "1.0.0",
|
||||
@@ -203,14 +168,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/cli": {
|
||||
"version": "4.20.4",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/cli/-/cli-4.20.4.tgz",
|
||||
"integrity": "sha512-hKE002Dm9AzWU62qpRyYpjQv8BIXG951Z0ToNdG9J1wvvJNYH7h6rGv7kdQMbgvXff/KOQvVdnGJZ023SrG+Jw==",
|
||||
"version": "4.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/cli/-/cli-4.20.5.tgz",
|
||||
"integrity": "sha512-/HSpueCFS+SvXas9FwfP8qoD9skhdTDE5r7dT9Xlrs7HGCBjxDYvSiw6tW0GIjfw3/49Yf8TCVDFBsISang1hg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@sasjs/adapter": "^4.19.0",
|
||||
"@sasjs/core": "5.2.9",
|
||||
"@sasjs/lint": "2.5.0",
|
||||
"@sasjs/adapter": "4.19.1",
|
||||
"@sasjs/core": "5.2.10",
|
||||
"@sasjs/lint": "4.1.0",
|
||||
"@sasjs/utils": "3.6.2",
|
||||
"chalk": "4.1.2",
|
||||
"dotenv": "17.4.2",
|
||||
@@ -236,11 +201,28 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/cli/node_modules/@sasjs/core": {
|
||||
"version": "5.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-5.2.9.tgz",
|
||||
"integrity": "sha512-R5wtJF0ANHchaURqTF2t1vRXrhUJ+uDfj21ewzdOq4Lp14CWW22+yYeRQooY4QOpyHUyFDjReA5q6SoJt2Tz1g==",
|
||||
"version": "5.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-5.2.10.tgz",
|
||||
"integrity": "sha512-Lk/ENHVuXaeAl8M0+Mjh1rcBSdFt4tIM6OPxPPmk78ZD0gYFqAOUZreqIoV9EN+VwPDqYsUPDqXzh0crW6j8OA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sasjs/cli/node_modules/shelljs": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz",
|
||||
"integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"glob": "^7.0.0",
|
||||
"interpret": "^1.0.0",
|
||||
"rechoir": "^0.6.2"
|
||||
},
|
||||
"bin": {
|
||||
"shjs": "bin/shjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/core": {
|
||||
"version": "5.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-5.2.8.tgz",
|
||||
@@ -248,14 +230,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sasjs/lint": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/lint/-/lint-2.5.0.tgz",
|
||||
"integrity": "sha512-bVUtYnKhCigXylCCvQ6dsAIywpPKkX3BXatMqOWjDVw9R30jfj4M3/qn05fcR45L42Y7e5Tuzlt+LYs+9k95Yg==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/lint/-/lint-4.1.0.tgz",
|
||||
"integrity": "sha512-Vpg1s0/zdPMFJ0hRXNXk53HhE3kfRr5gFRS0kvrse+bxgcbqih1qqVW7yZ2krhSCYylp9z13eBXLhXbRrj70VQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sasjs/utils": "3.6.2",
|
||||
"ignore": "7.0.8"
|
||||
"ignore": "7.0.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/utils": {
|
||||
@@ -390,13 +372,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
|
||||
"integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz",
|
||||
"integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.5",
|
||||
"form-data": "^4.0.6",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
@@ -420,6 +402,12 @@
|
||||
"tough-cookie": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
@@ -451,16 +439,14 @@
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.21",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz",
|
||||
"integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
@@ -646,26 +632,18 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/consola": {
|
||||
"version": "2.15.0",
|
||||
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.0.tgz",
|
||||
"integrity": "sha512-vlcSGgdYS26mPf7qNi+dCisbhiyDnrN1zaRbw3CSuc2wGOMEGGPsp46PdRG5gqXwgtJfjxDkxRNAgRPr1B77vQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/cssstyle": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
|
||||
@@ -840,66 +818,6 @@
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/execa": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
||||
"integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.3",
|
||||
"get-stream": "^6.0.0",
|
||||
"human-signals": "^2.1.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"merge-stream": "^2.0.0",
|
||||
"npm-run-path": "^4.0.1",
|
||||
"onetime": "^5.1.2",
|
||||
"signal-exit": "^3.0.3",
|
||||
"strip-final-newline": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
"glob-parent": "^5.1.2",
|
||||
"merge2": "^1.3.0",
|
||||
"micromatch": "^4.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz",
|
||||
"integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/find": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/find/-/find-0.3.0.tgz",
|
||||
@@ -910,9 +828,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz",
|
||||
"integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -959,6 +877,12 @@
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -1014,28 +938,25 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
@@ -1184,15 +1105,6 @@
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=10.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
@@ -1226,20 +1138,55 @@
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "7.0.8",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.8.tgz",
|
||||
"integrity": "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==",
|
||||
"version": "7.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.11.tgz",
|
||||
"integrity": "sha512-YChdK5txDjwGUvgR7oCJcLGkwi3LDh8Zx8tS7ndYciLEg/oOCL26VUBSUEtgS7EjiybcBqFxh5j0rAdDX7/bbg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/interpret": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
|
||||
"integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.17.0",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz",
|
||||
"integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hasown": "^2.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-docker": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
|
||||
@@ -1255,15 +1202,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
@@ -1273,18 +1211,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-interactive": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
|
||||
@@ -1294,33 +1220,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-potential-custom-element-name": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-unicode-supported": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
|
||||
@@ -1345,12 +1250,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/js-base64": {
|
||||
"version": "3.9.3",
|
||||
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.9.3.tgz",
|
||||
@@ -1532,34 +1431,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
@@ -1590,6 +1461,18 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -1630,24 +1513,21 @@
|
||||
"is-wsl": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-run-path": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
|
||||
"integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/nwsapi": {
|
||||
"version": "2.2.27",
|
||||
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.27.tgz",
|
||||
"integrity": "sha512-gQPNF78qebCQ6tvVFBYrvJdBNOrYZm90ZlXgpIFm06p6qHDHq/XC4TnJftN6OMbxVE0UTBAoRgcsDeJBBooITw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/onetime": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
||||
@@ -1744,33 +1624,27 @@
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-parse": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pend": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/prompts": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
||||
@@ -1820,26 +1694,6 @@
|
||||
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
@@ -1854,6 +1708,17 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/rechoir": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
|
||||
"integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==",
|
||||
"dependencies": {
|
||||
"resolve": "^1.1.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@@ -1869,6 +1734,27 @@
|
||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.12",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
|
||||
"integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"is-core-module": "^2.16.1",
|
||||
"path-parse": "^1.0.7",
|
||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"resolve": "bin/resolve"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/restore-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
|
||||
@@ -1882,45 +1768,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"iojs": ">=1.0.0",
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rrweb-cssom": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
||||
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@@ -1959,40 +1812,6 @@
|
||||
"node": ">=v12.22.7"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shelljs": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.10.0.tgz",
|
||||
"integrity": "sha512-Jex+xw5Mg2qMZL3qnzXIfaxEtBaC4n7xifqaqtrZDdlheR70OGkydrPJWT0V1cA1k3nanC86x9FwAmQl6w3Klw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"execa": "^5.1.1",
|
||||
"fast-glob": "^3.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
@@ -2049,15 +1868,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-final-newline": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
|
||||
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -2070,6 +1880,18 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-preserve-symlinks-flag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/symbol-tree": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||
@@ -2094,18 +1916,6 @@
|
||||
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
|
||||
@@ -2249,21 +2059,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
@@ -2281,6 +2076,12 @@
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.21.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
|
||||
|
||||
+3
-2
@@ -30,10 +30,11 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@sasjs/cli": "4.20.4",
|
||||
"@sasjs/cli": "4.20.5",
|
||||
"@sasjs/core": "5.2.8"
|
||||
},
|
||||
"overrides": {
|
||||
"nanoid": "3.3.18"
|
||||
"nanoid": "3.3.18",
|
||||
"shelljs": "0.8.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
@file
|
||||
@brief migration script - raise the VIEW row limit, add the EDIT cell limit
|
||||
|
||||
OPTIONAL CHANGE - updates three options in MPE_CONFIG:
|
||||
|
||||
|option|old|new|
|
||||
|---|---|---|
|
||||
|DC_MAXOBS_WEBVIEW|500|2000|
|
||||
|DC_MAXOBS_WEBEDIT|100|250|
|
||||
|DC_MAXCELLS_WEBEDIT|(new)|200000|
|
||||
|
||||
MPE_CONFIG is bitemporal, so the current row for each option is closed and a
|
||||
new row written with the new value. A site that has already chosen its own
|
||||
values for these options will have them replaced - edit the values below
|
||||
first if that is not what you want.
|
||||
|
||||
An option that is switched off - a current row with var_active=0 - is left
|
||||
alone, and a note is written to the log. Writing an active row for it would
|
||||
silently switch it back on.
|
||||
|
||||
Without this script the code defaults still apply (the same numbers), but
|
||||
only for options that are not present at all - an existing install carries
|
||||
its own rows, so it keeps 500 / 100 until this is run.
|
||||
|
||||
**/
|
||||
|
||||
%let dclib=YOURDCLIB;
|
||||
%let now=%sysfunc(datetime());
|
||||
|
||||
libname &dclib "/YOUR/DATACONTROLLER/LIBRARY/PATH";
|
||||
|
||||
/* which of the three options the site has switched off */
|
||||
proc sql noprint;
|
||||
select count(*) into :off_view trimmed
|
||||
from &dclib..mpe_config
|
||||
where var_name='DC_MAXOBS_WEBVIEW' and var_active=0 and &now lt tx_to;
|
||||
select count(*) into :off_edit trimmed
|
||||
from &dclib..mpe_config
|
||||
where var_name='DC_MAXOBS_WEBEDIT' and var_active=0 and &now lt tx_to;
|
||||
select count(*) into :off_cells trimmed
|
||||
from &dclib..mpe_config
|
||||
where var_name='DC_MAXCELLS_WEBEDIT' and var_active=0 and &now lt tx_to;
|
||||
quit;
|
||||
|
||||
%if &off_view=0 %then %do;
|
||||
proc sql noprint;
|
||||
update &dclib..mpe_config
|
||||
set tx_to=&now
|
||||
where var_name='DC_MAXOBS_WEBVIEW' and var_active=1 and &now lt tx_to;
|
||||
|
||||
insert into &dclib..mpe_config
|
||||
set tx_from=&now
|
||||
,tx_to='31DEC9999:23:59:59'dt
|
||||
,var_scope="DC"
|
||||
,var_name="DC_MAXOBS_WEBVIEW"
|
||||
,var_value='2000'
|
||||
,var_active=1
|
||||
,var_desc='This sets the maximum number of observations that can be'
|
||||
!!' loaded into the browser in the VIEW screen.';
|
||||
quit;
|
||||
%end;
|
||||
%else %put NOTE: DC_MAXOBS_WEBVIEW is switched off (var_active=0) - not changed;
|
||||
|
||||
%if &off_edit=0 %then %do;
|
||||
proc sql noprint;
|
||||
update &dclib..mpe_config
|
||||
set tx_to=&now
|
||||
where var_name='DC_MAXOBS_WEBEDIT' and var_active=1 and &now lt tx_to;
|
||||
|
||||
insert into &dclib..mpe_config
|
||||
set tx_from=&now
|
||||
,tx_to='31DEC9999:23:59:59'dt
|
||||
,var_scope="DC"
|
||||
,var_name="DC_MAXOBS_WEBEDIT"
|
||||
,var_value='250'
|
||||
,var_active=1
|
||||
,var_desc='This sets the maximum number of observations that can be'
|
||||
!!' loaded into the browser for editing in the EDIT screen. The'
|
||||
!!' DC_MAXCELLS_WEBEDIT limit is applied at the same time, whichever'
|
||||
!!' is reached first.';
|
||||
quit;
|
||||
%end;
|
||||
%else %put NOTE: DC_MAXOBS_WEBEDIT is switched off (var_active=0) - not changed;
|
||||
|
||||
%if &off_cells=0 %then %do;
|
||||
proc sql noprint;
|
||||
update &dclib..mpe_config
|
||||
set tx_to=&now
|
||||
where var_name='DC_MAXCELLS_WEBEDIT' and var_active=1 and &now lt tx_to;
|
||||
|
||||
insert into &dclib..mpe_config
|
||||
set tx_from=&now
|
||||
,tx_to='31DEC9999:23:59:59'dt
|
||||
,var_scope="DC"
|
||||
,var_name="DC_MAXCELLS_WEBEDIT"
|
||||
,var_value='200000'
|
||||
,var_active=1
|
||||
,var_desc='This sets the maximum number of CELLS (rows multiplied by'
|
||||
!!' columns) that can be loaded into the browser for editing in the'
|
||||
!!' EDIT screen. It exists because a wide table is expensive long'
|
||||
!!' before it is tall - 250 rows of a 1000 column table is a quarter'
|
||||
!!' of a million cells. Whichever of this and DC_MAXOBS_WEBEDIT is'
|
||||
!!' reached first applies.';
|
||||
quit;
|
||||
%end;
|
||||
%else %put NOTE: DC_MAXCELLS_WEBEDIT is switched off (var_active=0) - not changed;
|
||||
@@ -72,12 +72,35 @@ insert into &lib..mpe_config set
|
||||
,tx_to='31DEC9999:23:59:59'dt
|
||||
,var_scope="DC"
|
||||
,var_name="DC_MAXOBS_WEBEDIT"
|
||||
,var_value="100"
|
||||
,var_value="250"
|
||||
,var_active=1
|
||||
,var_desc='This sets the maximum number of observations that can be loaded'
|
||||
!!' into the browser for editing in the EDIT screen. A higher number'
|
||||
!!' will require a decent browser (ie, not IE) and more memory on the'
|
||||
!!' client side.';
|
||||
!!' client side. The DC_MAXCELLS_WEBEDIT limit is applied at the same'
|
||||
!!' time, whichever is reached first.';
|
||||
insert into &lib..mpe_config set
|
||||
tx_from=0
|
||||
,tx_to='31DEC9999:23:59:59'dt
|
||||
,var_scope="DC"
|
||||
,var_name="DC_MAXCELLS_WEBEDIT"
|
||||
,var_value="200000"
|
||||
,var_active=1
|
||||
,var_desc='This sets the maximum number of CELLS (rows multiplied by'
|
||||
!!' columns) that can be loaded into the browser for editing in the EDIT'
|
||||
!!' screen. It exists because a wide table is expensive long before it'
|
||||
!!' is tall - 250 rows of a 1000 column table is a quarter of a million'
|
||||
!!' cells. Whichever of this and DC_MAXOBS_WEBEDIT is reached first'
|
||||
!!' applies.';
|
||||
insert into &lib..mpe_config set
|
||||
tx_from=0
|
||||
,tx_to='31DEC9999:23:59:59'dt
|
||||
,var_scope="DC"
|
||||
,var_name="DC_MAXOBS_WEBVIEW"
|
||||
,var_value="2000"
|
||||
,var_active=1
|
||||
,var_desc='This sets the maximum number of observations that can be'
|
||||
!!' loaded into the browser in the VIEW screen.';
|
||||
insert into &lib..mpe_config set
|
||||
tx_from=0
|
||||
,tx_to='31DEC9999:23:59:59'dt
|
||||
|
||||
@@ -4,9 +4,12 @@
|
||||
@details If user is in the administrator group, they can call this
|
||||
service directly adding the following URL params:
|
||||
|
||||
@li &flavour= (only PGSQL supported at this time)
|
||||
@li &flavour= (SAS, PGSQL or TSQL - defaults to SAS)
|
||||
@li &schema= (optional, if target schema is needed)
|
||||
|
||||
Data inserts are generated for the SAS and PGSQL flavours only; the TSQL
|
||||
export contains DDL without inserts.
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mf_getuser.sas
|
||||
@li mp_abort.sas
|
||||
|
||||
@@ -72,3 +72,34 @@ run;
|
||||
test=EQUALS 1,
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/* test 3 - TSQL flavour (DDL only, no inserts) */
|
||||
data work.params;
|
||||
length name $32 value $1000;
|
||||
name='flavour';value='TSQL';output;
|
||||
name='schema';value='public';output;
|
||||
run;
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputparams=work.params,
|
||||
outref=web3,
|
||||
viyaresult=WEBOUT_TXT,
|
||||
mdebug=&sasjs_mdebug
|
||||
)
|
||||
|
||||
data work.results3;
|
||||
infile web3;
|
||||
input;
|
||||
putlog _infile_;
|
||||
if index(upcase(_infile_),'CREATE TABLE') then do;
|
||||
putlog 'test passed';
|
||||
output;
|
||||
stop;
|
||||
end;
|
||||
run;
|
||||
|
||||
%mp_assertdsobs(work.results3,
|
||||
desc=TSQL flavour DDL file is successfully returned,
|
||||
test=EQUALS 1,
|
||||
outds=work.test_results
|
||||
)
|
||||
@@ -205,7 +205,14 @@ select upcase(loadtype)
|
||||
%put NOTE- Please add to &mpelib..MPE_CONFIG table;
|
||||
%put NOTE-;%put NOTE-;
|
||||
%global DC_MAXOBS_WEBEDIT;
|
||||
%let DC_MAXOBS_WEBEDIT=500;
|
||||
%let DC_MAXOBS_WEBEDIT=250;
|
||||
%end;
|
||||
%if not %symexist(DC_MAXCELLS_WEBEDIT) %then %do;
|
||||
%put NOTE:;%put NOTE- DC_MAXCELLS_WEBEDIT not found!;
|
||||
%put NOTE- Please add to &mpelib..MPE_CONFIG table;
|
||||
%put NOTE-;%put NOTE-;
|
||||
%global DC_MAXCELLS_WEBEDIT;
|
||||
%let DC_MAXCELLS_WEBEDIT=200000;
|
||||
%end;
|
||||
/* for tables which use RKs/SKs then we just expose the business key to
|
||||
users - this lets uploads be sent to multiple environments (with
|
||||
@@ -254,18 +261,31 @@ select upcase(loadtype)
|
||||
,msg=%str(Issue with filtering (line 165) )
|
||||
)
|
||||
|
||||
options obs=&DC_MAXOBS_WEBEDIT;
|
||||
/* Sort first - the PRE_EDIT_HOOK contract is that it runs on sorted rows -
|
||||
then run the hook, then check the row and cell limits. Checking once,
|
||||
after both, means the counts cover everything that could reach the
|
||||
browser: the rows the selection matched, and anything the hook has added
|
||||
to work.out. A wide table exhausts the cell budget long before it
|
||||
reaches the row limit - 250 rows of a 1000 column table is a quarter of
|
||||
a million cells - so capping rows alone does not bound the work the
|
||||
browser is asked to do. The payload itself is capped further down, when
|
||||
work.outdata is built. */
|
||||
%let sortpk=%sysfunc(coalescec(&sortpk &var_busfrom,_ALL_));
|
||||
proc sort data=work.out; by &sortPK; run;
|
||||
options obs=max;
|
||||
|
||||
%mpe_runhook(PRE_EDIT_HOOK)
|
||||
|
||||
%let obscnt=%mf_getattrn(work.out,NLOBS);
|
||||
%let colcnt=%sysfunc(countw(%mf_getvarlist(work.out)));
|
||||
%let cellcnt=%eval(&obscnt * &colcnt);
|
||||
%mp_abort(iftrue=(&obscnt>&DC_MAXOBS_WEBEDIT)
|
||||
,mac=&_program
|
||||
,msg=Table is too big (&obscnt rows) - please filter and try again!
|
||||
)
|
||||
%mp_abort(iftrue=(&cellcnt>&DC_MAXCELLS_WEBEDIT)
|
||||
,mac=&_program
|
||||
,msg=Selection is too wide (&obscnt rows x &colcnt cols) - please filter and try again!
|
||||
)
|
||||
|
||||
|
||||
/* order delete var and pk fields at start of table */
|
||||
@@ -274,6 +294,9 @@ select upcase(loadtype)
|
||||
,Str2= _____DELETE__THIS__RECORD_____ &pk
|
||||
);
|
||||
%put sourcevars=&sourcevars;
|
||||
/* the payload is capped here, at the row limit - the limits above are
|
||||
checked against the whole selection, this is what goes to the browser */
|
||||
options obs=&DC_MAXOBS_WEBEDIT;
|
||||
data outdata;
|
||||
/* delete & pk fields come first */
|
||||
attrib _____DELETE__THIS__RECORD_____ &pk label='';
|
||||
@@ -288,6 +311,7 @@ select upcase(loadtype)
|
||||
%end;
|
||||
set work.out ;
|
||||
run;
|
||||
options obs=max;
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<h4> SAS Macros </h4>
|
||||
@li dc_assignlib.sas
|
||||
@li mp_abort.sas
|
||||
@li mp_getcols.sas
|
||||
@li mp_validatecol.sas
|
||||
|
||||
@version 9.2
|
||||
@@ -43,32 +44,33 @@ run;
|
||||
|
||||
%dc_assignlib(READ,%scan(&ds,1,.))
|
||||
|
||||
proc contents noprint data=&ds
|
||||
out=droplist1 (keep=name type length label varnum format:);
|
||||
run;
|
||||
data cols(keep=name type length varnum format label);
|
||||
set droplist1(rename=(format=format2 type=type2));
|
||||
name=upcase(name);
|
||||
if type2=2 then do;
|
||||
length format $49.;
|
||||
if format2='' then format=cats('$',length,'.');
|
||||
else if formatl=0 then format=cats(format2,'.');
|
||||
else format=cats(format2,formatl,'.');
|
||||
type='C';
|
||||
ddtype='C';
|
||||
end;
|
||||
else do;
|
||||
if format2='' then format=cats(length,'.');
|
||||
else if formatl=0 then format=cats(format2,'.');
|
||||
else if formatd=0 then format=cats(format2,formatl,'.');
|
||||
else format=cats(format2,formatl,'.',formatd);
|
||||
type='N';
|
||||
if format=:'DATETIME' then ddtype='DATETIME';
|
||||
else if format=:'DATE' then ddtype='DATE';
|
||||
else if format=:'TIME' then ddtype='TIME';
|
||||
else ddtype='N';
|
||||
end;
|
||||
if label='' then label=name;
|
||||
/* Column metadata, typed the way the editor's own load types it: %mp_getcols
|
||||
infers DATE and DATETIME across the whole format list (YYMMDD, MMDDYY,
|
||||
E8601DA, B8601DA, NLDATE, NLDATM, E8601DT, ...), which a test against the
|
||||
format prefix does not - a column formatted YYMMDD would otherwise reach the
|
||||
picker as numeric. getdata.sas makes the same call for the same reason. */
|
||||
%mp_getcols(&ds, outds=cols1)
|
||||
|
||||
data cols(keep=name type length varnum fmtname ddtype);
|
||||
/* type arrives from %mp_getcols as a $1 holding 'C' or 'N', so it has to be
|
||||
widened before the client vocabulary below is assigned to it: a $1
|
||||
variable truncates 'num' and 'char' to 'n' and 'c', and the picker
|
||||
compares them exactly (query.component for the operator set, sas-store
|
||||
for the quoting). ddtype needs no statement - it is created by the macro
|
||||
with 'CHARACTER', so it is already $9. */
|
||||
length type $4;
|
||||
set cols1;
|
||||
/* DDTYPE in the client's vocabulary, as getdata.sas maps it */
|
||||
if ddtype='CHARACTER' then ddtype='C';
|
||||
else if ddtype='NUMERIC' then ddtype='N';
|
||||
/* TYPE in the client's vocabulary too. On the normal path mergeColsRules()
|
||||
sets it from the loaded table's formats - 'num' for the date and time
|
||||
types, the column's own type otherwise - and the picker keys both its
|
||||
operator set (query.component) and its value quoting (sas-store) on it.
|
||||
The fallback list never reaches that pass, because the load that would
|
||||
have fed it is the one that failed, so the service has to carry it. */
|
||||
if ddtype in ('DATE','DATETIME','TIME') or ddtype='N' then type='num';
|
||||
else type='char';
|
||||
run;
|
||||
|
||||
%mp_abort(iftrue= (&syscc ne 0)
|
||||
|
||||
@@ -66,8 +66,20 @@ run;
|
||||
)
|
||||
|
||||
%let nobs=0;
|
||||
%let nbad=0;
|
||||
proc sql noprint;
|
||||
select count(*) into: nobs from web2.cols;
|
||||
/**
|
||||
* The vocabulary the picker matches on, and the mapping it is derived from.
|
||||
* TYPE is assigned in a step that SETs the %mp_getcols output, where type is
|
||||
* a $1 holding 'C'/'N' - without the LENGTH statement that widens it, the
|
||||
* values truncate to 'n'/'c' and the client's exact comparisons (query
|
||||
* component for the operator set, sas-store for the quoting) stop matching.
|
||||
* The mock derives TYPE in JS, so only this suite can catch that.
|
||||
*/
|
||||
select count(*) into: nbad from web2.cols
|
||||
where (ddtype='C' and type ne 'char')
|
||||
or (ddtype in ('N','DATE','DATETIME','TIME') and type ne 'num');
|
||||
quit;
|
||||
|
||||
%mp_assert(
|
||||
@@ -76,6 +88,12 @@ quit;
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(&nbad=0),
|
||||
desc=TYPE vocabulary intact - every column is num or char, per its ddtype,
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/**
|
||||
* dump results to the log for offline inspection
|
||||
*/
|
||||
|
||||
@@ -181,7 +181,7 @@ run;
|
||||
%put NOTE- Please add to &mpelib..MPE_CONFIG table;
|
||||
%put NOTE-;%put NOTE-;
|
||||
%global DC_MAXOBS_WEBVIEW;
|
||||
%let DC_MAXOBS_WEBVIEW=500;
|
||||
%let DC_MAXOBS_WEBVIEW=2000;
|
||||
%end;
|
||||
%if &existds>0 %then %do;
|
||||
|
||||
|
||||
Reference in new issue
Block a user