Merge branch 'demodata' into fix/audit-20260112
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m15s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m43s
Build / Build-and-test-development (pull_request) Successful in 9m56s

This commit is contained in:
2026-02-07 23:46:10 +00:00
11 changed files with 893 additions and 165 deletions

View File

@@ -228,6 +228,8 @@ jobs:
cp sasjs/utils/favicon.ico ../client/dist/favicon.ico
sasjs c -t server
rm -rf sasjsbuild/tests
server_apploc="/Public/app/dc"
sed -i "s|apploc=\"[^\"]*\"|apploc=\"${server_apploc}\"|g" sasjsbuild/services/web/index.html
sasjs b -t server
cp sasjsbuild/server.json.zip ./sasjs_server.json.zip

View File

@@ -1,3 +1,17 @@
## [7.2.8](https://git.datacontroller.io/dc/dc/compare/v7.2.7...v7.2.8) (2026-02-06)
### Bug Fixes
* bump adapter version ([f4c8699](https://git.datacontroller.io/dc/dc/commit/f4c8699aaf0b1e01b447296978a4f6dedc8903f9))
## [7.2.7](https://git.datacontroller.io/dc/dc/compare/v7.2.6...v7.2.7) (2026-02-05)
### Bug Fixes
* dclib not found error in getchangeinfo job ([86791db](https://git.datacontroller.io/dc/dc/commit/86791dbaca39034a19bf8f34efbddf898c57f2f7))
## [7.2.6](https://git.datacontroller.io/dc/dc/compare/v7.2.5...v7.2.6) (2026-01-05)

531
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -51,7 +51,7 @@
"@clr/icons": "^13.0.2",
"@clr/ui": "file:libraries/clr-ui-17.9.0.tgz",
"@handsontable/angular-wrapper": "16.0.1",
"@sasjs/adapter": "^4.16.0",
"@sasjs/adapter": "^4.16.2",
"@sasjs/utils": "^3.5.3",
"@sheet/crypto": "file:libraries/sheet-crypto.tgz",
"@types/d3-graphviz": "^2.6.7",

View File

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

2
sas/package-lock.json generated
View File

@@ -243,6 +243,7 @@
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"peer": true,
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
@@ -1761,6 +1762,7 @@
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
"integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"psl": "^1.1.33",
"punycode": "^2.1.1",

View File

@@ -28,6 +28,7 @@
@li mp_abort.sas
@li mf_existvar.sas
@li mf_getattrn.sas
@li mf_getengine.sas
@li mf_getuser.sas
@li mf_getvartype.sas
@li mp_lockanytable.sas
@@ -70,13 +71,16 @@
* perform basic checks
*/
/* do tables exist? */
%if not %sysfunc(exist(&base_lib..&base_dsn)) %then %do;
%mp_abort(msg=&base_lib..&base_dsn does not exist)
%end;
%else %if %sysfunc(exist(&append_lib..&append_dsn))=0
and %sysfunc(exist(&append_lib..&append_dsn,VIEW))=0 %then %do;
%mp_abort(msg=&append_lib..&append_dsn does not exist)
%end;
%mp_abort(
iftrue=(%sysfunc(exist(&base_lib..&base_dsn)) ne 1),
msg=&base_lib..&base_dsn does not exist
)
%mp_abort(
iftrue=(%sysfunc(exist(&append_lib..&append_dsn))=0
and %sysfunc(exist(&append_lib..&append_dsn,VIEW))=0 ),
msg=&append_lib..&append_dsn does not exist
)
/* do TX columns exist? */
%if &loadtype ne UPDATE %then %do;
%if not %mf_existvar(&base_lib..&base_dsn,&tech_from) %then %do;
@@ -111,55 +115,83 @@ data _null_;
gap=intck('HOURS',now,datetime());
call symputx('gap',gap,'l');
run;
%mf_abort(
%mp_abort(
iftrue=(&gap > 24),
msg=NOW variable (&now) is not within a 24hr tolerance
)
/* have any warnings / errs occurred thus far? If so, abort */
%mf_abort(
%mp_abort(
iftrue=(&syscc>0),
msg=Aborted due to SYSCC=&SYSCC status
)
/**
* Create closeout statements. These are sent as individual SQL statements
* Create closeout statements. If UPDATE approach and CAS engine, use the
* DeleteRows action (as regular SQL deletes are not supported).
* Otherwise, the deletions are sent as individual SQL statements
* to ensure pass-through utilisation. The update_cnt variable monitors
* how many records were actually updated on the target table.
*/
%local update_cnt;
%local update_cnt etype;
%let update_cnt=0;
%let etype=%mf_getengine(&base_lib);
%put &=etype;
filename tmp temp;
data _null_;
set ___closeout1;
file tmp;
if _n_=1 then put 'proc sql noprint;' ;
length string $32767.;
%if &loadtype=UPDATE %then %do;
put "delete from &base_lib..&base_dsn where 1";
%end;
%else %do;
now=symget('now');
put "update &base_lib..&base_dsn set &tech_to= " now @;
%if %mf_existvar(&base_lib..&base_dsn,PROCESSED_DTTM) %then %do;
put " ,PROCESSED_DTTM=" now @;
%end;
put " where " now " lt &tech_to ";
%end;
%do x=1 %to %sysfunc(countw(&PK));
%let var=%scan(&pk,&x,%str( ));
%if %mf_getvartype(&base_lib..&base_dsn,&var)=C %then %do;
/* use single quotes to avoid ampersand resolution in data */
string=" & &var='"!!trim(prxchange("s/'/''/",-1,&var))!!"'";
%if &loadtype=UPDATE and &etype=CAS %then %do;
/* create temp table for deletions */
%local delds;%let delds=%mf_getuniquename(prefix=DC);
data casuser.&delds;
set work.___closeout1;
run;
/* build the proc */
data _null_;
file tmp;
put 'proc cas;table.deleteRows result=r/ table={' ;
put " caslib='&base_lib',name='&base_dsn',where='1=1',";
put " whereTable={caslib='CASUSER',name='&delds'}";
put "};";
put "call symputx('update_cnt',r.RowsDeleted);";
put "quit;";
put '%put &=update_cnt;';
put "proc sql;drop table CASUSER.&delds;";
stop;
run;
%end;
%else %do;
data _null_;
set ___closeout1;
file tmp;
if _n_=1 then put 'proc sql noprint;' ;
length string $32767.;
%if &loadtype=UPDATE %then %do;
put "delete from &base_lib..&base_dsn where 1";
%end;
%else %do;
string=cats(" & &var=",&var);
now=symget('now');
put "update &base_lib..&base_dsn set &tech_to= " now @;
%if %mf_existvar(&base_lib..&base_dsn,PROCESSED_DTTM) %then %do;
put " ,PROCESSED_DTTM=" now @;
%end;
put " where " now " lt &tech_to ";
%end;
put string;
%end;
put "&filter ;";
put '%let update_cnt=%eval(&update_cnt+&sqlobs);%put update_cnt=&update_cnt;';
run;
%do x=1 %to %sysfunc(countw(&PK));
%let var=%scan(&pk,&x,%str( ));
%if %mf_getvartype(&base_lib..&base_dsn,&var)=C %then %do;
/* use single quotes to avoid ampersand resolution in data */
string=" & &var='"!!trim(prxchange("s/'/''/",-1,&var))!!"'";
%end;
%else %do;
string=cats(" & &var=",&var);
%end;
put string;
%end;
put "&filter ;";
put '%let update_cnt=%eval(&update_cnt+&sqlobs);';
put '%put update_cnt=&update_cnt;';
run;
%end;
data _null_;
infile tmp;

View File

@@ -51,7 +51,7 @@
%local audtab;
proc sql noprint;
select coalescec(audit_libds,"&dc_libref..MPE_AUDIT") into: audtab
from &dclib..MPE_TABLES
from &dc_libref..MPE_TABLES
where &dc_dttmtfmt. lt tx_to
and libref="%scan(&libds,1,.)" and dsn="%scan(&libds,2,.)";
%if "&audtab"="0" %then %do;

View File

@@ -202,7 +202,7 @@
},
{
"name": "server",
"serverUrl": "https://sas9.4gl.io",
"serverUrl": "https://sas.4gl.io",
"serverType": "SASJS",
"httpsAgentOptions": {
"rejectUnauthorized": false,

View File

@@ -0,0 +1,386 @@
/**
@file
@brief Creates demo tables and associated config
@details Can be removed in prod installs.
To activate this job, set dcdemoflag=1
Note that this will:
* REPLACE any tables named CARS_EXT or COUNTRIES in the PUBLIC library
* REPLACE all DC config for libraries named PUBLIC
* CREATE a folder called "demo" in the DC Apploc
<h4> SAS Macros </h4>
@li mpeinit.sas
@li mf_getengine.sas
@li mf_getuser.sas
@li mf_increment.sas
@li mf_nobs.sas
@li mf_uid.sas
@li mp_abort.sas
@li mp_binarycopy.sas
@li mp_replace.sas
@li mx_createwebservice.sas
@author 4GL Apps Ltd
@copyright 4GL Apps Ltd. This code may only be used within Data Controller
and may not be re-distributed or re-sold without the express permission of
4GL Apps Ltd.
**/
%let dcdemoflag=1;
%let demolib=PUBLIC;
options dlcreatedir;
%mpeinit()
/* to enable - delete here */
%let dcdemoflag=0;
libname &demolib "%sysfunc(pathname(&dc_libref))/&demolib";
%mp_abort(iftrue= (&dcdemoflag ne 1)
,mac=&_program
,msg=%str(Job not configured. See comments in the code.)
)
data work.cars_ext(index=(carspk=(make model PRODUCTIONDATE) /unique));
attrib
MAKE length= $13
MODEL length= $40
TYPE length= $8
ORIGIN length= $6
COUNTRY length= $30
POTENTIALBUY length= $6
COMMENT length= $30
NOTES length= $30
CHECKBOXVAR length= $3
PRODUCTIONDATE length= 8 format=DATE9.
EDITOR length= $30
APPROVER length= $30
;
set sashelp.cars;
retain comment 'n/a';
if mod(ceil(ranuni(1)*100),3)=0 then notes=catx(' ',make,type);
call missing(notes);
/* random / reproducible date between 1960 and 2020 */
PRODUCTIONDATE=ceil(ranuni(1)*365*60);
if mod(ceil(ranuni(1)*1000),2)=0 then CHECKBOXVAR='YES';
else CHECKBOXVAR='No';
if mod(ceil(ranuni(1)*1000),3)=0 then POTENTIALBUY='Maybe';
else if mod(ceil(ranuni(1)*1000),2)=0 then POTENTIALBUY='Yes';
else POTENTIALBUY='No';
make=cats(make);
model=cats(model);
EDITOR='SYSTEM';
APPROVER='n/a';
array cntrs (4) $ 60 _temporary_ ( "Germany" "France" "Poland" "Italy");
if origin='USA' then country='USA';
else if origin='Asia' then do;
if mod(_n_,2)=0 then country='Japan';
else country='Korea';
end;
else COUNTRY = cntrs[ ceil(dim(cntrs) * ranuni(1))];
*put (_all_)(=);
run;
data work.COUNTRIES(index=(countriespk=(origin country) /unique));
attrib
ORIGIN length= $6
COUNTRY length= $30
EDITOR length= $30
APPROVER length= $30
;
infile cards dsd;
input
ORIGIN :$char.
COUNTRY :$char.
;
EDITOR='SYSTEM';
APPROVER='n/a';
datalines4;
Europe,Germany
Europe,France
Europe,Poland
Europe,Italy
USA,USA
Asia,Japan
Asia,Korea
;;;;
run;
data work.jobdata;
length message job $100;
call missing(of _all_);
run;
%let engine_type=%mf_getengine(&demolib);
%put &=engine_type;
%if &engine_type=CAS %then %do;
proc cas;
table.tableExists result=r / name="CARS_EXT" caslib="PUBLIC";
if r.exists then
table.dropTable / name="CARS_EXT" caslib="PUBLIC" quiet=TRUE;
table.tableExists result=r2 / name="COUNTRIES" caslib="PUBLIC";
if r2.exists then
table.dropTable / name="COUNTRIES" caslib="PUBLIC" quiet=TRUE;
table.tableExists result=r3 / name="JOBDATA" caslib="PUBLIC";
if r3.exists then
table.dropTable / name="JOBDATA" caslib="PUBLIC" quiet=TRUE;
quit;
proc casutil;
load data=work.CARS_EXT outcaslib="PUBLIC" casout="CARS_EXT" promote;
load data=work.COUNTRIES outcaslib="PUBLIC" casout="COUNTRIES" promote;
load data=work.COUNTRIES outcaslib="PUBLIC" casout="JOBDATA" promote;
run;
%end;
%else %do;
options replace;
data &demolib..CARS_EXT; set work.cars_ext;
data &demolib..COUNTRIES; set work.countries;
data &demolib..JOBDATA; set work.JOBDATA;run;
%end;
%let apploc=%mf_getapploc(&_program);
%let demolib=%upcase(&demolib);
proc sql;
delete from &dc_libref..mpe_tables
where libref="&demolib" and dsn in ('CARS_EXT','COUNTRIES');
data append;
if 0 then set &dc_libref..mpe_tables;
TX_FROM=0;
TX_TO='31DEC9999:23:59:59'dt;
LIBREF="&demolib";
LOADTYPE='UPDATE';
NUM_OF_APPROVALS_REQUIRED=1;
PRE_EDIT_HOOK="&apploc/demo/PREEDIT";
POST_EDIT_HOOK="&apploc/demo/POSTEDIT";
PRE_APPROVE_HOOK="&apploc/demo/PREAPPROVE";
POST_APPROVE_HOOK="&apploc/demo/POSTAPPROVE";
DSN='CARS_EXT'; BUSKEY='MAKE MODEL PRODUCTIONDATE'; output;
DSN='COUNTRIES'; BUSKEY='ORIGIN COUNTRY'; output;
run;
proc append base=&dc_libref..MPE_TABLES data=&syslast;
run;
/* hard coded values for CHECKBOXVAR */
%let rk=1e6;
proc sql noprint;
delete from &dc_libref..mpe_selectbox
where select_lib="&demolib"
and select_ds in ('CARS_EXT');
select max(selectbox_rk) into: rk
from &dc_libref..mpe_selectbox;
insert into &dc_libref..mpe_selectbox set
selectbox_rk=%mf_increment(rk)
,ver_from_dttm=0
,select_lib="&demolib"
,select_ds="CARS_EXT"
,base_column="CHECKBOXVAR"
,selectbox_value='Yes'
,selectbox_order=1
,ver_to_dttm='31DEC5999:23:59:59'dt;
insert into &dc_libref..mpe_selectbox set
selectbox_rk=%mf_increment(rk)
,ver_from_dttm=0
,select_lib="&demolib"
,select_ds="CARS_EXT"
,base_column="CHECKBOXVAR"
,selectbox_value='No'
,selectbox_order=2
,ver_to_dttm='31DEC5999:23:59:59'dt;
/* Table driven values */
delete from &dc_libref..MPE_VALIDATIONS
where base_lib="&demolib" and base_ds="CARS_EXT";
insert into &dc_libref..MPE_VALIDATIONS set
tx_from=0
,base_lib="&demolib"
,base_ds="CARS_EXT"
,base_col="MAKE"
,rule_type='HARDSELECT'
,rule_value="SASHELP.CARS.MAKE"
,rule_active=1
,tx_to='31DEC5999:23:59:59'dt;
insert into &dc_libref..MPE_VALIDATIONS set
tx_from=0
,base_lib="&demolib"
,base_ds="CARS_EXT"
,base_col="MODEL"
,rule_type='HARDSELECT'
,rule_value="SASHELP.CARS.MODEL"
,rule_active=1
,tx_to='31DEC5999:23:59:59'dt;
insert into &dc_libref..MPE_VALIDATIONS set
tx_from=0
,base_lib="&demolib"
,base_ds="CARS_EXT"
,base_col="TYPE"
,rule_type='SOFTSELECT'
,rule_value="SASHELP.CARS.TYPE"
,rule_active=1
,tx_to='31DEC5999:23:59:59'dt;
insert into &dc_libref..MPE_VALIDATIONS set
tx_from=0
,base_lib="&demolib"
,base_ds="CARS_EXT"
,base_col="POTENTIALBUY"
,rule_type='SOFTSELECT'
,rule_value="&demolib..CARS_EXT.POTENTIALBUY"
,rule_active=1
,tx_to='31DEC5999:23:59:59'dt;
insert into &dc_libref..MPE_VALIDATIONS set
tx_from=0
,base_lib="&demolib"
,base_ds="CARS_EXT"
,base_col="COMMENT"
,rule_type='NOTNULL'
,rule_value="n/a"
,rule_active=1
,tx_to='31DEC5999:23:59:59'dt;
insert into &dc_libref..MPE_VALIDATIONS set
tx_from=0
,base_lib="&demolib"
,base_ds="CARS_EXT"
,base_col="ENGINESIZE"
,rule_type='MINVAL'
,rule_value="1.3"
,rule_active=1
,tx_to='31DEC5999:23:59:59'dt;
insert into &dc_libref..MPE_VALIDATIONS set
tx_from=0
,base_lib="&demolib"
,base_ds="CARS_EXT"
,base_col="ENGINESIZE"
,rule_type='MAXVAL'
,rule_value="8.3"
,rule_active=1
,tx_to='31DEC5999:23:59:59'dt;
%mp_abort(iftrue= (&syscc ne 0)
,mac=&_program
,msg=%str(syscc=syscc=&syscc during param configuration)
)
/* programmatic values for COUNTRY (Dynamic Dropdown) */
filename vldtr temp;
data _null_;
file vldtr ;
put 'proc sql;';
put 'create table work.vals as';
put ' select distinct ORIGIN as display_value,';
put ' ORIGIN as raw_value';
put " from &demolib..COUNTRIES";
put ' order by 1;';
put 'data work.DYNAMIC_VALUES; set work.vals;display_index=_n_;run;';
put ' ';
put 'proc sql;';
put 'create table work.dev as ';
put ' select a.display_index,b.country as display_value';
put ' from work.DYNAMIC_VALUES as a';
put " left join &demolib..countries as b";
put " on a.raw_value=b.origin";
put ' order by display_index;';
put 'data work.DYNAMIC_EXTENDED_VALUES; set work.dev;by display_index;';
put ' EXTRA_COL_NAME="COUNTRY";';
put ' DISPLAY_TYPE="C";';
put ' RAW_VALUE_CHAR=DISPLAY_VALUE;';
put ' RAW_VALUE_NUM=.;';
put ' if first.display_index then forced_value=1;';
put 'run;';
run;
%mx_createwebservice(path=&apploc/demo
,name=origin,code=vldtr
)
proc sql;
insert into &dc_libref..MPE_VALIDATIONS set
tx_from=0
,base_lib="&demolib"
,base_ds="CARS_EXT"
,base_col="ORIGIN"
,rule_type='HARDSELECT_HOOK'
,rule_value="&apploc/demo/origin"
,rule_active=1
,tx_to='31DEC5999:23:59:59'dt;
/* PRE_EDIT JOB */
%let fvar=XXXXXXXXXXX; /* cannot substitute macvars in parmcards */
filename ft15f001 temp;
parmcards4;
proc sql;
insert into XXXXXXXXXXX.JOBDATA values(
"&orig_libds (%mf_nobs(work.out) obs) fetched for editing %trim(
)by %mf_getUser() at %mf_uid()","&pgmloc");
;;;;
filename f1 temp;
%mp_binarycopy(inref=ft15f001, outref=f1)
%mp_replace("%sysfunc(pathname(f1))", findvar=fvar, replacevar=demolib)
%mx_createwebservice(path=&apploc/demo,name=PREEDIT,code=f1)
filename ft15f001 clear;
/* POST EDIT JOB */
filename ft15f001 temp;
parmcards4;
/* modify staging_ds to apply changes */
data work.staging_ds;
set work.staging_ds;
if "%scan(&orig_libds,1,.)"="XXXXXXXXXXX" then do;
if "%scan(&orig_libds,2,.)" in ('CARS_EXT','COUNTRIES')
then EDITOR="%mf_getuser()";
end;
run;
proc sql;
insert into XXXXXXXXXXX.JOBDATA values(
"&orig_libds (%mf_nobs(work.staging_ds) obs) staged %trim(
)by %mf_getUser() at %mf_uid()","&pgmloc");
;;;;
filename f2 temp;
%mp_binarycopy(inref=ft15f001, outref=f2)
%mp_replace("%sysfunc(pathname(f2))", findvar=fvar, replacevar=demolib)
%mx_createwebservice(path=&apploc/demo,name=POSTEDIT,code=f2)
filename ft15f001 clear;
/* PRE APPROVE JOB */
filename ft15f001 temp;
parmcards4;
/* modify staging_ds to apply changes */
data work.staging_ds;
set work.staging_ds;
if "%scan(&orig_libds,1,.)"="XXXXXXXXXXX" then do;
if "%scan(&orig_libds,2,.)" in ('CARS_EXT','COUNTRIES')
then APPROVER="%mf_getuser()";
end;
run;
proc sql;
insert into XXXXXXXXXXX.JOBDATA values(
"&orig_libds (%mf_nobs(work.staging_ds) obs) under review by %trim(
)by %mf_getUser() at %mf_uid()","&pgmloc");
;;;;
filename f3 temp;
%mp_binarycopy(inref=ft15f001, outref=f3)
%mp_replace("%sysfunc(pathname(f3))", findvar=fvar, replacevar=demolib)
%mx_createwebservice(path=&apploc/demo,name=PREAPPROVE,code=f3)
filename ft15f001 clear;
/* POST APPROVE JOB */
filename ft15f001 temp;
parmcards4;
proc sql;
insert into XXXXXXXXXXX.JOBDATA values(
"&orig_libds (%mf_nobs(work.staging_ds) obs) approved by %trim(
)by %mf_getUser() at %mf_uid()","&pgmloc");
;;;;
filename f4 temp;
%mp_binarycopy(inref=ft15f001, outref=f4)
%mp_replace("%sysfunc(pathname(f4))", findvar=fvar, replacevar=demolib)
%mx_createwebservice(path=&apploc/demo,name=POSTAPPROVE,code=f4)
filename ft15f001 clear;

View File

@@ -1,17 +1,14 @@
/**
@file dc_createdataset.sas
<h4> SAS Macros </h4>
@version 9.3
@author 4GL Apps Ltd
@copyright 4GL Apps Ltd. This code may only be used within Data Controller
and may not be re-distributed or re-sold without the express permission of
4GL Apps Ltd.
**/
%macro dc_createdataset(libds=mm_getlibs);
data viewdata;
%macro dc_createdataset(libds=mm_getlibs,outds=viewdata);
data &outds;
var1='Table';
var2="&libds";
var3="does not exist!";