Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f3c6f30a9 | ||
|
|
2eb5d74f59 | ||
|
|
bef1b99c78 | ||
|
|
5980e2caba | ||
|
|
27a6d98d0c | ||
|
|
59f169e32d | ||
|
|
1a5338e876 | ||
|
|
bbb163c25c | ||
|
|
2c7a3b8465 | ||
|
|
d37538264a | ||
|
|
cd5eeafa08 | ||
|
|
4ef2b5333a | ||
|
|
3d2ec5869b | ||
|
|
6cabe6796a | ||
|
|
ce479a2c51 | ||
|
|
bc27894b38 | ||
|
|
f38b5d795c | ||
|
|
67cf263ec5 | ||
|
|
db969f51b4 | ||
|
|
8d4a82f19a | ||
|
|
cb072bf60a | ||
|
|
365b178496 | ||
|
|
b75e0bb38e | ||
|
|
b61ec07c87 | ||
|
|
6469ad00a3 | ||
|
|
ed20e0b982 | ||
|
|
42cf023efb | ||
|
|
ae3f5128c2 | ||
|
|
bc44d4cf73 | ||
|
|
838bc7a96f | ||
|
|
53c0de0960 | ||
|
|
549ecc36aa | ||
|
|
463de81ffa | ||
|
|
79d2e887d3 | ||
|
|
1119645a00 | ||
|
|
a1513b871e | ||
|
|
59ff2b13fc | ||
|
|
7d950cbdbe | ||
|
|
66d5fecb0e | ||
|
|
0b18658686 | ||
|
|
bbd5793d8b | ||
|
|
0bda92b091 | ||
|
|
bf9fca1b9a | ||
|
|
d542ac442f | ||
|
|
0ee0b4ea7c | ||
|
|
2fe1bc2eea | ||
|
|
3e39339332 | ||
|
|
6d554930be | ||
|
|
a56c2f8d48 | ||
|
|
27ff40b739 | ||
|
|
4937edd7f4 | ||
|
|
fcba390804 | ||
|
|
8b6e3e60bc | ||
|
|
d244a34fc9 | ||
|
|
686a745f0b | ||
|
|
510947d173 | ||
|
|
589a745c5b | ||
|
|
d04c55c731 | ||
|
|
a6a6abf009 | ||
|
|
88368fa31d | ||
|
|
fdb1db216d | ||
|
|
6d833f9654 | ||
|
|
6bcd0adf81 | ||
|
|
93348ad604 | ||
|
|
7561bb0c39 | ||
|
|
959a9ffab4 | ||
|
|
2348575737 | ||
|
|
b2aac21b90 | ||
|
|
438b1e93d7 | ||
|
|
674737f840 | ||
|
|
150be89246 | ||
|
|
b6fea27963 | ||
|
|
aa1f015f5a | ||
|
|
79b62e4cfc | ||
|
|
73d4796638 | ||
|
|
7267af58c4 | ||
|
|
e5dfcd84da | ||
|
|
02295334b3 | ||
|
|
e7a538ed9a | ||
|
|
60a19dda89 | ||
|
|
b415793eec | ||
|
|
d352ea8fea | ||
|
|
9e04cd4220 | ||
|
|
181c3ae834 | ||
|
|
0ec0789ac4 | ||
|
|
2e5fc4a368 | ||
|
|
3d66f91820 | ||
|
|
5cdfadf75c | ||
|
|
fef2c3c12e | ||
|
|
b50efc01e2 | ||
|
|
de23966721 | ||
|
|
9121bf900d | ||
|
|
56175353d6 | ||
|
|
26b4c55ecd | ||
|
|
5be23393d6 | ||
|
|
bf3ef8bab1 | ||
|
|
729437308a | ||
|
|
244829edf9 | ||
|
|
b51c831b53 | ||
|
|
8e0b28ed93 | ||
|
|
5b6575ae8e | ||
|
|
9dcedfffb0 | ||
|
|
34846e5a4c |
No files matched your search
@@ -0,0 +1,63 @@
|
||||
# LinkedIn Post Writing Guide
|
||||
|
||||
Guidance for drafting LinkedIn posts promoting Data Controller (datacontroller.io).
|
||||
|
||||
## Audience
|
||||
|
||||
Our readers are typically:
|
||||
|
||||
- **Technical managers and platform owners** in regulated industries (Banking, Insurance, Pharma) running enterprise SAS® estates.
|
||||
- **Data governance / data quality leads** dealing with End User Computing (EUC) risk and audit requirements.
|
||||
- **SAS developers and administrators** who maintain data pipelines and care about automation, security and auditability.
|
||||
- Occasionally **C-level / procurement** - keep business value (OpEx, risk reduction, compliance) visible alongside technical detail.
|
||||
|
||||
Implications:
|
||||
|
||||
- Assume competence - no need to explain what ETL, SAS, or a data warehouse is.
|
||||
- Speak to their pains: regulatory reporting deadlines, audit trails, hand-cranked ETL, EUC sprawl, OpEx, blame-games between business and IT.
|
||||
- Professional but direct tone. Avoid hype, buzzword stuffing, and exclamation-mark overload.
|
||||
|
||||
## Structure
|
||||
|
||||
1. **Hook** - first 1-2 lines must stand alone; LinkedIn truncates the rest behind "…see more". State the pain or the claim, not the product.
|
||||
2. **Problem** - short paragraphs describing the pain in the reader's context.
|
||||
3. **Belief / shift** - what we believe the world should look like.
|
||||
4. **Solution** - introduce Data Controller, one or two sentences on what it does (zero-code data capture, review/approve workflow, audit trail, loads into SAS / any database).
|
||||
5. **Outcome** - the concrete result (eliminated ETL flows, reduced OpEx, full lineage).
|
||||
6. **CTA** - single call to action, typically "book a Discovery Call - link in the comments below 👇". LinkedIn deprioritises posts with external links, so put URLs in the first comment, not the body.
|
||||
|
||||
## Formatting capabilities and tricks
|
||||
|
||||
LinkedIn posts are plain text - there is no rich-text editor, so **no markdown**. Bold, italics and other styling you see in posts are faked with Unicode characters:
|
||||
|
||||
- **Faux bold / italic / monospace via Unicode Mathematical Alphanumeric Symbols** - e.g. `𝐛𝐨𝐥𝐝` (U+1D401+), `𝑖𝑡𝑎𝑙𝑖𝑐` (U+1D456+), `𝚖𝚘𝚗𝚘𝚜𝚙𝚊𝚌𝚎` (U+1D670+). Tools like YayText / LingoJam generate these. **Use with extreme restraint:** they break screen readers and accessibility tools, are not searchable (LinkedIn search treats them as different characters, so keywords in faux-bold won't be found), and can render as boxes on some devices. Our default: avoid them; use CAPS sparingly for emphasis instead. If a hook or single key term really needs to pop, one short faux-bold phrase is the acceptable ceiling.
|
||||
- **Bullets** - no native lists; use literal characters: `-`, `•`, `✓`, `→`, numbered `1.` etc. Each on its own line.
|
||||
- **Whitespace** - blank lines survive and are the primary layout tool; use them aggressively (1-3 sentence paragraphs). LinkedIn collapses multiple blank lines beyond one, so a single blank line is the unit.
|
||||
- **Emoji** - work natively and render fine; use sparingly and professionally (👇 for the CTA, occasional ✅). Avoid emoji walls.
|
||||
- **Links** - URLs in the body get auto-linked but LinkedIn deprioritises posts containing external links; put the URL in the first comment and point to it ("link in the comments 👇"). Tag people/companies with `@mention` (native, drives reach) where relevant.
|
||||
- **Truncation** - only the first ~3 lines (~210 characters on mobile) show before "…see more". The hook must earn the click; never waste the first line on a greeting or context-setting.
|
||||
- **Special characters** - Unicode symbols like → ✅ ⏱ work fine and can structure a post; avoid exotic characters that some fonts render as tofu (□).
|
||||
- **No nested structure** - no headings, tables, quotes or code blocks. Simulated section breaks (a line of `—` or just blank space) are the only separators.
|
||||
|
||||
## Formatting rules
|
||||
|
||||
- **Hashtags are written as `#tag`** (e.g. `#endusercomputing`, `#excel`, `#datagovernance`, `#sas`, `#masterdata`). Never write `hashtag#tag` - that is an artefact of copy-pasting from LinkedIn's editor UI and must be stripped/normalised to `#tag` before posting or republishing.
|
||||
- 3-5 hashtags, all lowercase, placed at the end on their own lines.
|
||||
- Use regular dashes (`-`), not em-dashes (`—`).
|
||||
- Short paragraphs (1-3 sentences) with blank lines between them - LinkedIn has no markdown.
|
||||
- No markdown syntax (no `**bold**`, no `[links](url)`) - LinkedIn renders plain text. Use CAPS sparingly for emphasis instead.
|
||||
- ® symbol on first mention of SAS® is fine; don't repeat it every time.
|
||||
- Keep total length under ~1,300 characters where possible; longer posts are fine when the story warrants it, but the hook still has to earn the "see more" click.
|
||||
|
||||
## Media
|
||||
|
||||
- Always attach an image - posts with images get substantially more reach. Use the same image as the feed post's `previewImg` where one exists.
|
||||
- **Optimum image size: 1200 x 627 px (1.91:1 landscape)** - LinkedIn's recommended size for feed images and link previews, displays uncropped on desktop and mobile. Square (1080 x 1080) and portrait (1080 x 1350, 4:5) also work and take up more feed real estate on mobile; avoid narrow/tall images beyond 4:5 as they get cropped. Keep key content away from the edges.
|
||||
- Product screenshots work well for feature announcements; flat branded illustrations for thought-leadership posts.
|
||||
|
||||
## Relationship to the site Feed
|
||||
|
||||
- Feed posts (`content/feed/`) and LinkedIn posts are siblings, not copies: the site version may contain markdown links and omits hashtags; the LinkedIn version uses plain text, `#tag` hashtags, and "link in the comments".
|
||||
- When converting site → LinkedIn: strip markdown, convert links to a comment-based CTA, append hashtags.
|
||||
- When converting LinkedIn → site: strip `hashtag#` artefacts and hashtags, add proper markdown links, add front matter per the `add-feed-post` skill.
|
||||
- **Keep them in sync:** whenever a feed post is modified, update its `Source LinkedIn post:` comment in the same change (same points, same claims, same ordering of lists) - and vice versa. A stale LinkedIn comment means the next social post is drafted from outdated copy.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Data Controller - Sales & Positioning Playbook
|
||||
|
||||
Reference for writing marketing, blog, feed, and sales copy. Covers pain points, target audience, value proposition, packaging, sales motion, and objection handling. Synthesised from the marketing site (`datacontroller.io`), product docs (`docs.datacontroller.io`), and product glossary (`dc/CONTEXT.md`).
|
||||
|
||||
## The product in one paragraph
|
||||
|
||||
Data Controller for SAS® is a SAS-Powered web application that lets business users safely add, modify and delete data in SAS datasets and databases. Every change goes through an edit-stage-approve workflow: edits are staged, validated at the point of entry, reviewed as a diff (new / changed / deleted rows), approved by a data owner, then loaded - with a full audit trail of who changed what, when, and why. It runs on SAS Viya, SAS 9 EBI and SASjs Server, entirely on-prem / offline (no external network calls). It can write to any database reachable via a SAS libname engine.
|
||||
|
||||
## The problem (pain points)
|
||||
|
||||
SAS users routinely need to load or fix data manually - steering tables, parameters, mappings, corrections, regulatory adjustments. The status-quo approaches all hurt:
|
||||
|
||||
- **Network drive + batch job** - drop a CSV/Excel on a share, wait for a nightly job. Every new input means a new job to build, test, document and deploy. File-format drift breaks batches at the worst times (month end).
|
||||
- **Direct SAS code / Enterprise Guide edits** - requires granting end users MODIFY access to sensitive data locations. High risk of locking datasets, no audit trail, no approval.
|
||||
- **Asking a DBA / IT to run a script** - change requests, queues, delays, blame games between business and IT.
|
||||
- **Bespoke in-house capture apps** - years of development and testing, and a "black box" nobody wants to maintain.
|
||||
- **EUC sprawl** - uncontrolled Excel/VBA applications on network shares: opaque access, no version control, no lineage, audit failure. Regulators care (SOX, BCBS, national DQ circulars). Fines are real: Morgan Stanley $5m (2020), Citibank $400m (2020), DTCC £350k (2021).
|
||||
|
||||
The five measurable cost areas (see blog post "ROI and Payback"):
|
||||
|
||||
1. **Development time** - reduced to zero (zero-code, config-driven; the grid is generated dynamically from table metadata).
|
||||
2. **Deployment time** - reduced to zero (config change in the UI, not a code release).
|
||||
3. **Batch incidents** - invalid data cannot be submitted (validated on arrival).
|
||||
4. **Data quality issues** - schema-based automatic rules plus configurable frontend validations and backend hook scripts.
|
||||
5. **Compliance costs** - single source of truth, full audit trail, Excel originals retained with each submission; no reperformed audits of unsecured EUC code.
|
||||
|
||||
## Target audience
|
||||
|
||||
**Verticals:** regulated industries with large SAS estates - Banking, Insurance, Pharma / Life Sciences. Typical use cases: regulatory reporting adjustments, actuarial / bitemporal reporting (Allianz), DWH steering-table management (AFA försäkring), data catalogs and lineage (Siemens Healthineers), BICC / BI competency centres (DER Touristik).
|
||||
|
||||
**Personas:**
|
||||
|
||||
- **Economic buyer:** Head of BI / Data Platform owner, CDO, Head of Regulatory Reporting. Cares about OpEx, risk reduction, audit outcomes, compliance.
|
||||
- **Champion:** SAS platform owner / technical manager drowning in ad-hoc data-load requests.
|
||||
- **End users:** business users (Editors) submitting data; data owners (Approvers) signing off; Auditors reviewing history. Roles: Viewer, Editor, Approver, Auditor, Administrator.
|
||||
- **Gatekeepers:** IT security (satisfied by: on-prem, no external calls, uses existing SASLogon auth, row & column level security, end users never get direct write access to target tables).
|
||||
|
||||
**Audience tone (from the LinkedIn guide):** assume competence - don't explain ETL or data warehouses. Speak to their pains: reporting deadlines, audit trails, hand-cranked ETL, EUC risk, OpEx. Professional and direct; no hype.
|
||||
|
||||
## Value proposition / key messages
|
||||
|
||||
- **Zero code** - adding a new editable table is configuration, done in the tool itself. Column types, lengths, dropdowns are derived from the target table at runtime.
|
||||
- **Review / approve workflow** - nothing is applied without 1+ approvals; approvers see only the diff.
|
||||
- **Full audit history** - who, when, why, what; original uploaded files retained.
|
||||
- **Secure** - existing SAS authentication; RLS and CLS enforced server-side; no end-user write access to targets.
|
||||
- **Real-time** - intra-day concurrent updates via lock table and queuing; loads abort if the table changed since the diff.
|
||||
- **Any database** - via SAS libname engines; supports retained keys, composite keys, SCD2 (TXTEMPORAL) and full BITEMPORAL history.
|
||||
- **Validation at point of entry** - HARDREGEX/SOFTREGEX, dropdowns, min/max, NOT NULL, plus hook scripts to run arbitrary SAS programs before/after edit or approve.
|
||||
- **Saves vs alternatives** - years of dev/test vs bespoke build; no black box.
|
||||
|
||||
## Packaging & pricing
|
||||
|
||||
Four packages, all with **unlimited users** (since v6), all flavours of SAS (Viya / EBI / SASjs Server):
|
||||
|
||||
| Package | Price | Shape |
|
||||
| --- | --- | --- |
|
||||
| Community | FREE | 35 tables/library, 15 view rows, 5 upload rows, 1 viewbox, best-endeavours support |
|
||||
| Bronze | 12k EUR pa | 1 production env, 100 tables/library, 100 rows, 8hr SLA, SASjs support |
|
||||
| Silver | 21k EUR pa | 2 production envs, unlimited tables/rows, 6 viewboxes, in-house SAS app support |
|
||||
| Gold | Contact us | Unlimited envs, 4hr SLA, developer + consulting days, **perpetual licence option** |
|
||||
|
||||
- Add-on: Developer Days (new product features) at 600 EUR/day for all plans; consulting rates at sasapps.io/pricing.
|
||||
- Pricing goals: Transparent, Simple, Fair. Full public price list on the site - a differentiator; use it.
|
||||
- Support: tickets via support.4gl.io or support@datacontroller.io, severity levels 1-4, SLAs per package, escalation by video call between managers.
|
||||
|
||||
## Sales motion / execution strategy
|
||||
|
||||
1. **Land with the free tier.** The Community package and the evaluation SPK (single-file import, ~minutes to install on SAS 9) make trying the product nearly frictionless. Since v6.1 the source is also available for anyone to build and evaluate. Push evaluation hard - it is the top of funnel. The eval version is deliberately not production-grade (BASE-engine config tables, table-lock risk, unlicensed interface) - the natural upgrade path is built in.
|
||||
2. **Discovery call as the primary CTA.** Marketing (LinkedIn, blog, feed) always ends with "book a Discovery Call". Content strategy: hook with the pain (EUC risk, ETL sprawl, audit pain), not the product.
|
||||
3. **Expand from one use case.** Typical wedge: steering tables / mapping tables / parameter tables in a reporting team. Expansion levers: more tables, more environments, more business units, API-driven submissions (API Explorer since v6).
|
||||
4. **Sell the business case, not features.** Use the ROI/Payback blog post (five cost areas + calculator) so the champion can build an internal business case. Quantify dev hours, batch incidents, audit effort.
|
||||
5. **Partner channel.** Resellers exist (e.g. SaasNow in the Netherlands); partner-purchased deals may have partner first-line support.
|
||||
6. **Roadmap as a sales tool.** The public roadmap is customer-driven - "features requested by customers get built" - and Gold includes Developer Days. Position the product as one that listens.
|
||||
7. **Proof points.** Named customer stories (AFA, Allianz, Siemens Healthineers, DER Touristik), SAS 2021 Excellence in Innovation Award finalist.
|
||||
|
||||
## Getting it installed (technical ease-of-adoption)
|
||||
|
||||
- Deploys entirely inside the customer's existing SAS platform - no new infrastructure, no external assets or network calls at runtime (passes strict security reviews).
|
||||
- SAS 9 EBI: SPK import via SAS Management Console / DI Studio; run the configurator stored process; done. Requirements documented at docs.datacontroller.io (`dci-requirements`, `dci-deploysas9`, `dci-stpinstance`).
|
||||
- Viya and SASjs Server deployments also documented (`deploy-viya`).
|
||||
- Instillation/deployment support is in-scope for all paid support plans.
|
||||
|
||||
## Licensing
|
||||
|
||||
- Annual subscription per package (Bronze/Silver/Gold); perpetual licence available at Gold.
|
||||
- Community tier is free; the evaluation build is governed by an evaluation agreement (Bowe IO Ltd) - evaluation/non-production use only, no redistribution, no reverse engineering, "as is".
|
||||
- Embedded Handsontable requires a licence key for commercial use - handled as part of paid deployments.
|
||||
|
||||
## Objection handling (from the FAQ)
|
||||
|
||||
- **"Can it change our data model?"** - No; it updates data, not structure. Positioned as a safety feature.
|
||||
- **"Our preferred database?"** - Yes, any engine SAS can reach (incl. Snowflake, Redshift support announcements).
|
||||
- **"Retained / surrogate keys?"** - Supported, along with composite primary keys, SCD2 and bitemporal loads.
|
||||
- **"Security review will block it."** - On-prem, offline, existing SASLogon, RLS/CLS, no direct target access, no metadata modification after install.
|
||||
- **"We could build this."** - Yes, at the cost of years of dev/test plus an unmaintainable black box; DC is proven, supported, documented, and source-available for evaluation.
|
||||
|
||||
## Copy rules when writing sales-adjacent content
|
||||
|
||||
- Regular dashes, not em-dashes. No hard-wrapped Markdown.
|
||||
- Use product glossary terms exactly (submission, staging, target table, load type, etc. - see `dc/CONTEXT.md`).
|
||||
- ® on first mention of SAS®.
|
||||
- CTA conventions per channel: site content may link directly; LinkedIn puts links in the first comment (see `.agents/docs/linkedin.md`).
|
||||
@@ -47,10 +47,16 @@ Front matter fields:
|
||||
| `author` | Yes | Displayed under the post title. |
|
||||
| `authorLink` | No | If set, the author name links out (e.g. to a LinkedIn profile). |
|
||||
| `tags` | Yes | One or more tags. Drives the sidebar category list and RSS category. |
|
||||
| `previewImg` | No | Relative path to an image in the same folder (e.g. `'./cover.png'`). Feed posts usually omit this since they're short-form. |
|
||||
| `previewImg` | No | Relative path to an image in the same folder (e.g. `'./cover.png'`). The template renders this image on the post page automatically - do NOT also embed the same image in the markdown body. |
|
||||
|
||||
Use regular dashes (`-`) in content, not em-dashes.
|
||||
|
||||
If the post is adapted from source copy the user provided (e.g. a LinkedIn post), paste the original text into an HTML comment (verbatim, except strip the literal `hashtag` prefix that LinkedIn sometimes adds before `#tags`, e.g. `hashtag#sas` -> `#sas`) (`<!-- ... -->`) at the bottom of the markdown file, labelled (e.g. `Source LinkedIn post:`). This preserves the original - including hashtags and checkmarks that get dropped/adapted in the site version - for future reuse.
|
||||
|
||||
**Important:** when editing an existing feed post that has a `Source LinkedIn post:` comment, ALWAYS update the LinkedIn version too, so the overall message stays consistent (same points, same claims, same ordering of any lists). Wording does not need to be identical, and formatting will differ: the article uses Markdown, while LinkedIn is plain text with `✓`-style bullets, hashtags, and a comment-based CTA (see `.agents/docs/linkedin.md`).
|
||||
|
||||
Every new feed post should include an image (a `previewImg` cover) - and its full image-generation prompt must be recorded in the same HTML comment block, labelled (e.g. `Image prompt:`), so it can be tweaked and regenerated later. If the user has not provided or asked for an image, propose one (theme, style and a draft prompt) and add the `Image prompt:` comment even if the image itself is not yet generated.
|
||||
|
||||
### 3. Add images (optional)
|
||||
|
||||
If you set `previewImg` or reference images in the body, place the image files in the same post directory and reference them with a relative path.
|
||||
|
||||
@@ -4,6 +4,14 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
# One deploy at a time: `surfer deploy` swaps the site atomically, so two runs
|
||||
# racing each other is not corruption, but it is a wasted build and the last
|
||||
# swap wins.
|
||||
concurrency:
|
||||
group: surfer-datacontroller-io
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
Build-and-publish:
|
||||
@@ -14,13 +22,48 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-deps
|
||||
|
||||
# Blocking gate. `npm audit --audit-level=<x>` is all-or-nothing and this
|
||||
# tree cannot pass a flat gate at any level: the Gatsby 5 build toolchain
|
||||
# carries ~100 advisories with no non-breaking fix, so gating on the tree
|
||||
# as a whole stops publishing permanently. scripts/audit-gate.mjs instead
|
||||
# accepts the known set (scripts/audit-baseline.json, with an owner and a
|
||||
# reason) and FAILS on any new critical/high advisory.
|
||||
- name: Audit for new critical/high advisories
|
||||
run: node scripts/audit-gate.mjs
|
||||
|
||||
# The prettier and type checks used to live in a GitHub workflow that
|
||||
# never ran on this forge; keep them ahead of the build so a type or
|
||||
# formatting error blocks the deploy rather than shipping.
|
||||
- name: Typecheck
|
||||
run: npm run typecheck
|
||||
|
||||
- name: Check prettier offences
|
||||
run: npm run lint
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
# Deploy tooling pinned to an exact version: the workflow publishes
|
||||
# whatever is committed, and an unpinned `latest` would let a breaking or
|
||||
# compromised upstream release ship straight to the live site.
|
||||
#
|
||||
# `deploy` (v7) replaces the site atomically - it extracts to a staging
|
||||
# folder and swaps it in, so a failed upload leaves the current site
|
||||
# serving. The v6 `put` this replaces was token authenticated and is
|
||||
# gone; v7 takes the Cloudron username plus an app password for this app.
|
||||
- name: Publish
|
||||
run: npx surfer put --token ${{ secrets.SURFER_TOKEN }} --server datacontroller.io public/* /
|
||||
env:
|
||||
SURFER_PASSWORD: ${{ secrets.SURFER_PASSWORD }}
|
||||
REF_NAME: ${{ gitea.ref_name }}
|
||||
COMMIT_SHA: ${{ gitea.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npx --yes @cloudron/surfer@7.2.0 deploy public \
|
||||
--server datacontroller.io \
|
||||
--username allan \
|
||||
--password "$SURFER_PASSWORD" \
|
||||
--message "Build from ${REF_NAME} @ ${COMMIT_SHA:0:8}"
|
||||
@@ -1,30 +0,0 @@
|
||||
name: Gatsby Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js from .nvmrc
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-deps
|
||||
|
||||
- name: Check prettier offences
|
||||
run: npm run lint
|
||||
|
||||
- uses: enriikke/gatsby-gh-pages-action@v2
|
||||
with:
|
||||
access-token: ${{ secrets.DCSITE }}
|
||||
deploy-branch: gh-pages
|
||||
gatsby-args: --prefix-paths
|
||||
@@ -0,0 +1,12 @@
|
||||
# Secret scanning for staged content, so a credential is caught before it is
|
||||
# committed rather than after it has been pushed and published.
|
||||
#
|
||||
# gitleaks is pinned to an exact tag: a floating rev would pull whatever the
|
||||
# hook's upstream does next into every contributor's commit path.
|
||||
#
|
||||
# Install once per clone: `pre-commit install`.
|
||||
repos:
|
||||
- repo: https://github.com/gitleaks/gitleaks
|
||||
rev: v8.30.1
|
||||
hooks:
|
||||
- id: gitleaks
|
||||
@@ -19,7 +19,7 @@ When writing marketing/blog/feed copy about a feature, treat `docs.datacontrolle
|
||||
All agent-facing material lives under `.agents/`:
|
||||
|
||||
- `.agents/skills/` - pi-discovered skills (e.g. `add-feed-post`).
|
||||
- `.agents/docs/` - reference docs consumed by skills and these instructions.
|
||||
- `.agents/docs/` - reference docs consumed by skills and these instructions. Notably `sales.md` (product positioning, pain points, audience, pricing and sales motion) - consult it when writing marketing, blog, feed or sales copy.
|
||||
|
||||
### Issue tracker
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 119 KiB |
@@ -0,0 +1,140 @@
|
||||
---
|
||||
title: 'Derived Variables: computed columns in the EDIT screen'
|
||||
description: Data Controller can now compute columns that do not exist in the table - and, crucially, filter on them. Configured per table in MPE_DERIVED, computed on every read, never stored.
|
||||
date: '2026-10-21 12:00:00'
|
||||
author: 'Allan Bowe'
|
||||
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||
previewImg: './derived-grid.png'
|
||||
tags:
|
||||
- Data Controller
|
||||
- Features
|
||||
---
|
||||
|
||||
# Derived variables: computed columns you can filter on
|
||||
|
||||
Every reporting team has a version of this request, and it always sounds like a five minute job: a column in the EDIT screen that is not in the table. A region worked out from a state code. A line value that is quantity times price. Not stored, not loaded, just *there* - and, as the requester usually adds, filterable.
|
||||
|
||||
The "not stored" part is easy. The "filterable" part is where the design lives, and it is worth writing down, because it explains every other decision in the feature.
|
||||
|
||||
## Why a computed column is not automatically filterable
|
||||
|
||||
You would write it in a DATA step:
|
||||
|
||||
```sas
|
||||
data out;
|
||||
set mylib.mytable;
|
||||
REGION = ifc(STATE in ('CA','OR','WA'),'West','East');
|
||||
where REGION = 'West';
|
||||
run;
|
||||
```
|
||||
|
||||
That fails. A `WHERE` statement applies to the data set named on the `SET` statement - the *input* - and on that input `REGION` does not exist. SAS cannot see a variable that the same step creates later. You can put the assignment and the filter in separate steps, but then the filter runs after the step has read every row, which is precisely the work the filter exists to avoid - and for a large table that is the difference between a fast screen and a coffee break.
|
||||
|
||||
So the calculation has to happen somewhere the filter can see it. In Data Controller it happens in a **DATA step view**:
|
||||
|
||||
```sas
|
||||
data work.vw_derived / view=work.vw_derived;
|
||||
set mylib.mytable;
|
||||
attrib REGION length=$8 label='REGION=ifc(...)' format=$8.;
|
||||
REGION = ifc(STATE in ('CA','OR','WA'),'West','East');
|
||||
run;
|
||||
|
||||
data out;
|
||||
set work.vw_derived;
|
||||
where %inc filtref;;
|
||||
run;
|
||||
```
|
||||
|
||||
The view is not executed when it is defined. When the second step reads it, the assignment runs as part of the view's step, so by the time the `WHERE` is evaluated `REGION` is a column of the input. The filter is unchanged, the row-level security clause is unchanged, and nothing extra is read.
|
||||
|
||||
## What it looks like
|
||||
|
||||
The demo table below has four rows. `LINE_VALUE` is `QTY * PRICE`, `REGION` is derived from `STATE`, and `REGION_LABEL` is built on `REGION`. None of the three is a column of the table, and all three are read-only in the grid - and marked so, with a tinted header carrying an `fx` badge, italic tinted cells, and an edge where the computed block begins, so that "computed" never has to be confused with "locked by a rule":
|
||||
|
||||

|
||||
|
||||
Because they are on the input to the filter, they filter like any other column. Here is `REGION = 'West'`, with the filter bar naming the derived variable:
|
||||
|
||||

|
||||
|
||||
and a numeric one, `LINE_VALUE > 50`:
|
||||
|
||||

|
||||
|
||||
The value list in the filter dialog is built the same way, from the table under the rest of the filter - so a derived column gets a dropdown of its distinct values, not a blank box.
|
||||
|
||||
## Configuration
|
||||
|
||||
`MPE_DERIVED` is itself a Data Controller table, so it is maintained through the EDIT screen with the usual submit / review / approve workflow. One row per variable - these are the three rows behind the screenshots above:
|
||||
|
||||

|
||||
|
||||
|
||||
```
|
||||
VAR_NM REGION
|
||||
VAR_DEPS STATE
|
||||
VAR_TYPE C
|
||||
VAR_LEN 8
|
||||
VAR_FMT (blank)
|
||||
VAR_LABEL (blank)
|
||||
VAR_LOGIC ifc(STATE in ('CA','OR','WA'),'West','East')
|
||||
ACTIVE 1
|
||||
```
|
||||
|
||||
and the chained one:
|
||||
|
||||
```
|
||||
VAR_NM REGION_LABEL
|
||||
VAR_DEPS REGION STATE
|
||||
VAR_TYPE C
|
||||
VAR_LEN 14
|
||||
VAR_FMT (blank)
|
||||
VAR_LABEL Region and state
|
||||
VAR_LOGIC cats(REGION,'-',STATE)
|
||||
ACTIVE 1
|
||||
```
|
||||
|
||||
Which produces, at read time:
|
||||
|
||||
```sas
|
||||
attrib REGION length=$8 label='REGION=ifc(STATE in (''CA'',''OR'',''WA''),''West'',''East'')' format=$8.;
|
||||
REGION = ifc(STATE in ('CA','OR','WA'),'West','East');
|
||||
attrib REGION_LABEL length=$14 label='Region and state' format=$14.;
|
||||
REGION_LABEL = cats(REGION,'-',STATE);
|
||||
```
|
||||
|
||||
Type and length are declared rather than guessed, so a value is truncated exactly as specified. The format is optional; when it is left blank a character variable gets `$<length>.` and a numeric one `<length>.`, and a date, time or datetime format makes the column render - and filter - as a date, the same way a stored column does. The label is `VAR_LABEL` when you supply one, otherwise the name, an equals sign and the expression, so a user who switches labels on can see where the value came from.
|
||||
|
||||
## Dependencies, without a sequence number
|
||||
|
||||
`REGION_LABEL` needs `REGION` to exist first. Rather than maintain a sort order, each row declares its inputs in `VAR_DEPS`, and the statements are emitted in dependency order. Adding a variable in the middle of a chain means editing one row's `VAR_DEPS`, not renumbering the table.
|
||||
|
||||
`VAR_DEPS` lists *every* input - physical columns and derived variables alike - and the declaration is checked against the expression in both directions: a derived variable the expression references must be declared, and a declared name the expression does not reference is an error. A declared name must be a column of the table or a derived variable of the same table, and a cycle is rejected with the path that forms it:
|
||||
|
||||
```
|
||||
VAR_DEPS contains a cycle: A -> B -> C -> A
|
||||
```
|
||||
|
||||
## What it will not do
|
||||
|
||||
**Expressions only.** `VAR_LOGIC` must be a single SAS expression. Semicolons, macro triggers (`%`, `&`) and comments are rejected, so one row can never be more than an expression - which is the point: a config table is not a place to write programs. Conditional logic is available through functions such as `ifc()`, `choosec()`, `cats()`, `catx()`, `put()`, `input()` and `intnx()`.
|
||||
|
||||
**No shadowing, no self-reference.** A derived variable may not reuse the name of a column in the target table. Nor may it reference itself: `SUMMER = SUMMER + 1` implies a retained value, which depends on the order rows happen to arrive in.
|
||||
|
||||
**Never stored.** Derived values are computed on every read. Nothing derived is submitted, reviewed or written back - the approval package contains physical columns only, and a crafted submission that includes a derived column is discarded before the security check runs.
|
||||
|
||||
**EDIT screen only, for now.** The VIEW screen and the download services do not show derived columns. A saved filter that references one still applies there and is evaluated the same way.
|
||||
|
||||
**Not for row level security.** A rule in `MPE_ROW_LEVEL_SECURITY` cannot reference a derived column: the rule is evaluated against the submitted data, where the column does not exist. Saving a rule that names one is rejected with an explanation rather than left to fail at submit time.
|
||||
|
||||
## One warning
|
||||
|
||||
An expression can reference a column that is hidden from a user by [Column Level Security](https://docs.datacontroller.io/column-level-security/) - and the derived column will show the result. Configure derived variables with the same care as any other data exposure.
|
||||
|
||||
## Availability
|
||||
|
||||
Derived variables are available on SAS 9, Viya and SASjs Server. The configuration table, the settings and the post-edit validation are created by the usual upgrade - there is nothing to install beyond the release itself.
|
||||
|
||||
The full reference, including every column of `MPE_DERIVED` and the validation rules applied at submit time, is in the [docs](https://docs.datacontroller.io/derived-variables/).
|
||||
|
||||
If a table of yours has been waiting for "just one more column", we would love to [hear from you](/contact).
|
||||
@@ -61,7 +61,7 @@ Did you know that, in addition to a regular missing value in SAS (`.`), there ar
|
||||
|
||||
These values can now be both viewed and edited in Data Controller following an update to the [SASjs Adapter](https://github.com/sasjs/adapter#variable-types).
|
||||
|
||||
`video: [Retain Formulas when Loading Excel to SAS](https://www.youtube-nocookie.com/embed/ggrcNr23Jzw)`
|
||||
`video: [Managing Special Missing Values with Data Controller for SAS](https://www.youtube-nocookie.com/embed/ggrcNr23Jzw)`
|
||||
|
||||
There is nothing extra to configure for special SAS numerics - they are simply available by default, for numeric cells.
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 57 KiB |
@@ -0,0 +1,199 @@
|
||||
---
|
||||
title: "v7.13 Release: Formulas & Regex"
|
||||
description: Data Controller 7.13 wires a spreadsheet-grade formula engine into the editor grid, completing the point-of-entry validation story that began with 7.12's regex rules. Both are pure MPE_VALIDATIONS configuration - no code, no deployment.
|
||||
date: '2026-09-03 12:00:00'
|
||||
author: 'Allan Bowe'
|
||||
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||
previewImg: './v713_cover.png'
|
||||
tags:
|
||||
- Releases
|
||||
- Data Controller
|
||||
---
|
||||
|
||||
# v7.13: Formulas & Regex
|
||||
|
||||
We stopped writing release-specific blog posts after v6.1 - the automated, public release system made them redundant for routine version bumps. But every so often a release lands that changes what the product can actually _do_, and v7.13 is one of them. Together with 7.12 it completes a piece of the [roadmap](https://docs.datacontroller.io/roadmap/) we have been chipping away at for a while: **frontend formulae and regex rules**, both configurable purely in `MPE_VALIDATIONS`.
|
||||
|
||||
If you configure data entry in Data Controller, this post is a practical guide: how the rules work, how to set them up, and the process flow from config to grid. All the screenshots below are captured from a live editor session against the demo tables, so what you see is what shipped.
|
||||
|
||||
## Why this matters
|
||||
|
||||
Data Controller's job is to let business users change data safely. A big part of "safely" is catching problems at the point of entry - rather than after an approval, in a batch log, or (worst case) in a report. The [validations](https://docs.datacontroller.io/dcc-validations/) framework already covered length, type, nullability, primary keys, ranges, casing and dropdowns. Two things were missing:
|
||||
|
||||
* **Computed values.** Plenty of tables have columns that are derived from other columns - a revenue column that is price times volume, a stamp column that records who last touched a row. Until now the options were a backend hook script (real SAS code to write, test and deploy) or just letting users type anything.
|
||||
* **Pattern enforcement.** A dropdown is overkill when all you need is "this value looks like an email address" or "this postcode is well-formed". You want the _shape_ of the value checked as typed - and sometimes you want a hard block, sometimes just a gentle warning.
|
||||
|
||||
7.12 delivered regex rules (`HARDREGEX` / `SOFTREGEX`). 7.13 delivers formulas (`HARDFORMULA` / `SOFTFORMULA`). Both are rows in a config table. That is the whole feature.
|
||||
|
||||
## The process flow: from MPE_VALIDATIONS to the grid
|
||||
|
||||
Every configurable rule in Data Controller follows the same pipeline, and the new rules plug straight into it:
|
||||
|
||||
1. **Configure.** `MPE_VALIDATIONS` is itself a Data Controller table - open it from the navigation tree like any other, and add a row with `BASE_LIB`, `BASE_DS` and `BASE_COL` pointing at the column you want to govern, `RULE_TYPE` set to the new rule, `RULE_VALUE` holding the formula or pattern, and `RULE_ACTIVE=1`. Submit, approve, done - it's a config change, not a code release. The [MPE_VALIDATIONS table guide](https://docs.datacontroller.io/tables/mpe_validations/) has the full column reference.
|
||||
2. **Serve.** When a user opens the editor, the [`editors/getdata`](https://code.datacontroller.io/getdata_8sas.html) service extracts the active rules for that table - it filters `MPE_VALIDATIONS` on library, table and `RULE_ACTIVE=1` - and returns them in the `dqrules` object of the response, alongside the table data, schema, and the schema-derived `NOTNULL` constraints.
|
||||
3. **Apply.** The frontend wires each rule into the Handsontable grid: formula rules are computed live by [HyperFormula](https://hyperformula.handsontable.com/) (the calculation engine behind Handsontable itself), regex rules are evaluated in the browser with the JavaScript regex engine.
|
||||
4. **Block or warn.** On submit, the standard [cell validation](https://docs.datacontroller.io/dcc-validations/) cycle runs: `HARD` rules block the submission if violated, `SOFT` rules warn but allow.
|
||||
|
||||
Here are the new rule types as they appear in the config - one row per rule, `RULE_VALUE` holding the formula or the pattern:
|
||||
|
||||

|
||||
|
||||
For regex there is also a config-time guard, and it's a neat piece of dogfooding: because the rule itself lives in a table, saving an edit to `MPE_VALIDATIONS` through Data Controller runs the [post-edit hook](https://code.datacontroller.io/mpe__validations__postedit_8sas.html), which passes every `HARDREGEX` / `SOFTREGEX` `RULE_VALUE` through `PRXPARSE` and rejects the edit if the pattern is invalid - listing the offending columns. A typo in a pattern is caught the moment you save the rule, not the first time a user hits it.
|
||||
|
||||
## Regex rules (HARDREGEX / SOFTREGEX, v7.12)
|
||||
|
||||
Regex rules validate cell values against a SAS (Perl-style) regular expression. You provide the pattern in `RULE_VALUE`; whether it blocks or warns depends on the rule type:
|
||||
|
||||
* **HARDREGEX** - the value **must** match the pattern. If it doesn't, the cell is highlighted red and submission is blocked.
|
||||
* **SOFTREGEX** - a non-matching value is highlighted yellow as a warning. The user can still submit - it's a nudge, not a block.
|
||||
|
||||
### Setting one up
|
||||
|
||||
Say `SOME_CHAR` in the demo table must contain either "the" or "data" (case-insensitive). That's one insert:
|
||||
|
||||
```sas
|
||||
insert into &lib..MPE_VALIDATIONS set
|
||||
tx_from=0
|
||||
,base_lib="&lib"
|
||||
,base_ds="MPE_X_TEST"
|
||||
,base_col="SOME_CHAR"
|
||||
,rule_type='HARDREGEX'
|
||||
,rule_value='/the|data/i'
|
||||
,rule_active=1
|
||||
,tx_to='31DEC5999:23:59:59'dt;
|
||||
```
|
||||
|
||||
That's a real row from the demo data, by the way - the shipped `MPE_X_TEST` table carries sample `HARDREGEX` and `SOFTREGEX` rules so you can try both without touching your own config.
|
||||
|
||||
In the editor it looks like this - an invalid email blocked red by `HARDREGEX`, an invalid postcode warned yellow by `SOFTREGEX`, and a value that passes:
|
||||
|
||||

|
||||
|
||||
Note the third column in that screenshot, `REGEX_BOTH_COL`. It carries _both_ a `HARDREGEX` and a `SOFTREGEX` rule - and shows neither warning. That's the precedence rule in action: only one regex is ever applied per column, and if both exist the `SOFTREGEX` is ignored entirely, so the column behaves exactly as if it were `HARDREGEX`-only.
|
||||
|
||||
### What to know before writing patterns
|
||||
|
||||
The full list of gotchas is in the [regex rules documentation](https://docs.datacontroller.io/dcc-validations/#regex-rules); the ones that matter most in practice:
|
||||
|
||||
* **Use the PRX delimiter form.** Patterns are authored exactly as [PRXPARSE](https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/lefunctionsref/p0s9ilagexmjl8n1u7e1t1jfnzlk.htm) accepts them: `/pattern/flags`, e.g. `/^\d+$/` for "integers only", `/the|data/i` for a case-insensitive contains. The config-time `PRXPARSE` check enforces this - a bare pattern without delimiters will be rejected when you save the rule. (The frontend tolerates bare patterns for backwards compatibility, but don't author new rules that way.)
|
||||
* **Anchor your own patterns.** The pattern is used **as authored** - it is not auto-anchored. `/the|data/i` matches "the" _anywhere_ in the value. If you want the whole value to match, include `^` and `$` yourself.
|
||||
* **Stick to the common subset.** The pattern is evaluated in the browser with the JavaScript regex engine, which shares SAS PRX's core syntax (character classes, quantifiers, groups, alternation, `^`/`$` anchors, `\d \w \s` and friends). Three unambiguous Perl-isms are translated automatically - a leading `(?i)` modifier, `\Q...\E` literal sequences, and `\A`/`\z` absolute anchors. But Perl-only constructs such as possessive quantifiers (`a++`) and atomic groups (`(?>...)`) pass the SAS-side check and then silently do nothing in the frontend - so just don't use them.
|
||||
* **Blank is exempt.** Blank values skip pattern matching on any column type (use the `NOTNULL` rule if you also need populated values). On numeric columns the plain SAS missing (`.`) is also exempt - but special missings (`.A`-`.Z`, `._`) are not: they're deliberately-set values, so your pattern needs to accommodate them.
|
||||
* **One regex per column.** As above - `HARDREGEX` wins when both are present, and the column-header info dropdown shows only the rule that is actually applied.
|
||||
* **Length limit.** `RULE_VALUE` is 128 characters, which constrains very long patterns.
|
||||
* **Deleted rows are exempt.** Cells in rows marked for deletion are not validated or warned (except primary key columns, which still are).
|
||||
|
||||
### Where regex beats a dropdown
|
||||
|
||||
For currency codes, country codes, account number formats, email shapes, or "must be an integer" - a regex is one row of config versus a 1000-value `HARDSELECT` dropdown. And unlike a dropdown, a regex catches _paste_ operations too, not just typed values.
|
||||
|
||||
## Formula rules (HARDFORMULA / SOFTFORMULA, v7.13)
|
||||
|
||||
Formula rules make a column compute itself from other columns in the same row - like a spreadsheet formula, but the formula lives in config and applies to every row. When a user opens the editor, the formula is evaluated live and the result is shown in each cell. Change an input, and every dependent cell recalculates on the spot.
|
||||
|
||||
### Writing a formula
|
||||
|
||||
Formulas use **column names, not cell references** - there is no need to know the grid layout. If you have `A_COL` and `B_COL`, a `FORMULA_HARD_COL` rule is simply:
|
||||
|
||||
```
|
||||
=A_COL * B_COL
|
||||
```
|
||||
|
||||
Each row calculates its own result: row 1's values, row 2's values, and so on. Under the hood each column name is translated to a row-relative cell reference (text inside quotes is left untouched) and handed to HyperFormula - so you get the full spreadsheet function library, `IF`, `SUM`, `ROUND`, `CONCAT` and friends, without writing any code.
|
||||
|
||||
The one syntax rule: **each column name must be surrounded by spaces**. `=MATCH( PRICE )` resolves the column reference; `=MATCH(PRICE)` does not - without the surrounding blanks the token isn't recognised as a column reference, and the formula errors rather than using the column's value. The spaces stop column names clashing with function names.
|
||||
|
||||
### HARD vs SOFT formulas
|
||||
|
||||
* **HARDFORMULA** - the column is read-only. The formula result is always shown and submitted; the user cannot change it. Think calculated amounts, or a `PROCESSED_BY` column.
|
||||
* **SOFTFORMULA** - the cell shows the formula result, but the user can type a different value if the computed one is wrong. Their value is submitted instead. Useful for derived defaults where the business occasionally needs to override.
|
||||
|
||||
### Special values
|
||||
|
||||
Formulas can reference three runtime values, resolved when the formula is evaluated:
|
||||
|
||||
* `DC.ROW_STATUS` - the row's current state: `M` (Modified), `A` (Added), `D` (Deleted), or `U` (Unchanged). A newly-added row is `A` from the moment it is created - there is no transient state before that.
|
||||
* `DC.USER_NAME` - the logged-in user id.
|
||||
* `DC.ORIG_VALUE` - the original cell value before the current edit.
|
||||
|
||||
Our demo formula table puts them all to work. There's a row-status column (`=DC.ROW_STATUS`), a user column (`=DC.USER_NAME`), and a `CHANGE_SUMMARY_COL` whose rule reads like a proper audit sentence:
|
||||
|
||||
```
|
||||
=IF( DC.ROW_STATUS ="U","unedited",
|
||||
DC.USER_NAME &" changed from "& DC.ORIG_VALUE )
|
||||
```
|
||||
|
||||
Here it is live in the editor. We edited `B_COL` on the second row from 10 to 25 - and the whole row reacted: `FORMULA_HARD_COL` recomputed to 50 (`A_COL * B_COL`), `FORMULA_SOFT_COL` recomputed to 27 (`A_COL + B_COL`), and the row status flipped from `U` to `M` - all instantly, all without touching a single line of SAS:
|
||||
|
||||

|
||||
|
||||
And here's the payoff of those `DC.*` references - the audit columns from the same table, same edit. `ROW_STATUS_COL` flipped to `M`, and the change summary resolved the `IF` formula above into a sentence - "sasdemo changed from orig-2":
|
||||
|
||||

|
||||
|
||||
Because `DC.ROW_STATUS` is a live reference, it updates as the user works: edit a cell and the stamp flips to your user id; cancel the edit and it reverts.
|
||||
|
||||
### Editor behaviour worth knowing
|
||||
|
||||
* When you paste a formula into the grid, column names are automatically translated so the formula works in its new position.
|
||||
* A cell that is overwritten by a formula is flagged (with the original value retained) so you can revert it.
|
||||
* Formula-looking values pasted from Excel are treated as plain data (not evaluated), unless you explicitly choose "Apply as formula" - a deliberate safety measure so a spreadsheet's internal formulas don't leak into your data as live rules.
|
||||
* When submitted, it's the formula's computed value that is sent to the backend, never the raw formula text (a primary key column always resolves its live formula immediately to the computed value so the submission keys are correct).
|
||||
|
||||
## How it works under the hood (briefly)
|
||||
|
||||
Two moving parts, and the boundary between them explains most of the gotchas above.
|
||||
|
||||
**Serving the rules.** [`getdata`](https://code.datacontroller.io/getdata_8sas.html) extracts the active `MPE_VALIDATIONS` rows for the target table and returns them in `dqrules`, along with the schema-derived `NOTNULL` constraints. Formulas and regex are frontend rules - they are evaluated in the browser, not in SAS - which is why they arrive as `dqrules` rather than as backend hook scripts.
|
||||
|
||||
**Evaluating them.** For formulas, the client translates each column name in `RULE_VALUE` to a row-relative cell reference and hands it to HyperFormula (wired into Handsontable's formulas plugin). For regex, the client parses the PRX `/pattern/flags` form, translates the three Perl-isms it can, and constructs a JavaScript `RegExp`. If a pattern still fails in the browser, the editor treats it as always-valid rather than breaking - a failed pattern never blocks a submission it shouldn't.
|
||||
|
||||
**Guarding the config.** Because `MPE_VALIDATIONS` is itself a Data Controller table, a [post-edit hook](https://code.datacontroller.io/mpe__validations__postedit_8sas.html) validates new rules: `PRXPARSE` checks every regex `RULE_VALUE`, and the edit is rejected with the offending columns listed. Invalid rules never reach users.
|
||||
|
||||
## Also in these releases
|
||||
|
||||
Beyond the headline features, the usual spread of hardening and polishing shipped along the way: row-header status cells are now colour-coded (with a `±` symbol for modified rows), CAS support landed for the `REPLACE` load type, Viya deploy diagnostics were improved, and a large tranche of dependency upgrades (Angular 20, Handsontable 18 pinned, sasjs core v5) keeps the audit trail clean. As ever, the full commit-by-commit detail is in the [release notes](https://git.datacontroller.io/dc/dc/releases).
|
||||
|
||||
## Upgrading
|
||||
|
||||
The frontend changes are included in the 7.13 release assets. The backend additions are **data-only, optional migrations**:
|
||||
|
||||
* The **v7.12 migration** adds `HARDREGEX` / `SOFTREGEX` to the `RULE_TYPE` dropdown in `MPE_VALIDATIONS` (and switches the `MPE_SECURITY.LIBREF` validation to a hook that lists all libraries).
|
||||
* The **v7.13 migration** adds `HARDFORMULA` / `SOFTFORMULA` to the same dropdown.
|
||||
|
||||
Both scripts are in [`sas/sasjs/db/migrations/`](https://git.datacontroller.io/dc/dc/src/branch/main/sas/sasjs/db/migrations) in the source repo, and they're worth running even if you don't plan to use the rules immediately - they only add dropdown values to `MPE_SELECTBOX`.
|
||||
|
||||
## Try it yourself
|
||||
|
||||
The shipped demo data includes the regex rules on `MPE_X_TEST`, so you can see them working without configuring anything: open the demo library, edit `MPE_X_TEST`, and try entering a `SOME_SHORTNUM` between 1 and 5 (blocked red - `HARDREGEX`), a `PRIMARY_KEY_FIELD` with a decimal point (warned yellow - `SOFTREGEX`), or a `SOME_CHAR` without "the" or "data" in it (blocked - `HARDREGEX`).
|
||||
|
||||
For formulas, add a rule to one of your own tables - the `REVENUE = PRICE * VOLUME` example above is a two-minute configuration, and the `DC.*` special values make audit-style columns almost free. Full reference in the [validations docs](https://docs.datacontroller.io/dcc-validations/).
|
||||
|
||||
As ever - if you'd like to see additional validation types, [get in touch](https://datacontroller.io/pricing). The roadmap is customer-driven, and the validations list keeps growing.
|
||||
|
||||
<!-- Source LinkedIn post:
|
||||
|
||||
Every SAS team has a spreadsheet that "helps" load data into production.
|
||||
|
||||
You know the one. Someone built it years ago, nobody fully understands it, and it writes straight to a table it probably shouldn't.
|
||||
|
||||
What if that spreadsheet was wrong BEFORE anyone could hit send?
|
||||
|
||||
That's the idea behind Data Controller - a web app for SAS that lets business users change data safely. Every change is checked at the point of entry, goes for approval, and lands in a full audit trail. No new infrastructure - it runs on the SAS you already have.
|
||||
|
||||
The latest release adds two things that used to require a developer:
|
||||
|
||||
→ Formulas. Type a rule like = PRICE * VOLUME and the revenue column computes itself - spreadsheet-style, in the browser, live.
|
||||
|
||||
→ Pattern checks. "Must be a valid email." "Must be an integer." One row of config. Bad values are blocked or flagged before submission - not found in next month's report.
|
||||
|
||||
Both are configuration, not code. If you can fill in a table, you can set them up.
|
||||
|
||||
We wrote up how it works (with live screenshots of a bad email being caught red-handed):
|
||||
|
||||
Link in the comments 👇
|
||||
|
||||
#sas #dataquality #endusercomputing #excel #datagovernance
|
||||
|
||||
First comment: https://datacontroller.io/v7-13-formulas-and-regex/
|
||||
-->
|
||||
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 119 KiB |
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: 'The column that is not in the table'
|
||||
description: 'Derived variables give the EDIT screen columns that do not exist in the table - computed on every read, never stored, and filterable like any other column. The full story is in the article.'
|
||||
date: '2026-10-27 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
- Features
|
||||
previewImg: './derived-fx.png'
|
||||
---
|
||||
|
||||
# The column that is not in the table
|
||||
|
||||
Every reporting team has a version of this request, and it always sounds like a five minute job: "can we have a region column? It is just the state, mapped. And a line value - quantity times price. We do not need it stored, we just need it on the screen."
|
||||
|
||||
It is never a five minute job - so Data Controller now does the obvious thing instead. You write the expression, and the EDIT screen shows the column: computed on every read, never stored, marked `fx` and read-only, so nobody has to wonder whether it is locked or simply has nothing to edit.
|
||||
|
||||
The part that took the thinking is that they filter like any other column. That needed a DATA step view rather than a data step, and the reasoning is worth reading: [Derived variables: computed columns you can filter on](/derived-variables/).
|
||||
|
||||
Reference: [Derived Variables](https://docs.datacontroller.io/derived-variables/).
|
||||
|
||||
If a table of yours has been waiting for "just one more column", we would love to [hear from you](/contact).
|
||||
|
||||
<!--
|
||||
Feed teaser for the article at /derived-variables/. The article carries the
|
||||
engineering - why a computed column is not automatically filterable, the DATA
|
||||
step view, the configuration table, the limits - so this post links to it
|
||||
rather than repeating it.
|
||||
|
||||
Image prompt (cover): a clean, modern illustration on a white background: a
|
||||
spreadsheet grid rendered in soft slate and light blue, with the leftmost four
|
||||
columns showing plain data and three further columns floating slightly to the
|
||||
right, detached from the table, tinted light blue and italicised, each marked
|
||||
with a small "fx" badge. A thin left border separates the floating block from
|
||||
the stored columns. No text other than the fx badges. Flat vector style, brand
|
||||
palette: slate #314351, light blue #d3e5f2, accent green #79a843 used sparingly
|
||||
as a single dot.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -3,9 +3,10 @@ title: 'Complex Excel Uploads with Excel Maps'
|
||||
description: Extract scattered, dynamic data from any Excel workbook using configurable Excel Maps - no manual reformatting required.
|
||||
date: '2026-07-26 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data_controller
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './dc-capture-review-approve.jpeg'
|
||||
---
|
||||
|
||||
# Complex Excel Uploads with Excel Maps
|
||||
|
||||
|
After Width: | Height: | Size: 33 KiB |
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: 'Data Controller for SAS® - a Customer Story'
|
||||
description: Data Controller for SAS® lets business users self serve their data updates - restricted access to the data values, with the data model protected.
|
||||
date: '2026-10-11 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './customer-story.jpeg'
|
||||
---
|
||||
|
||||
# Data Controller for SAS® - a Customer Story
|
||||
|
||||
Data Controller for SAS® empowers business users to self serve their data updates, by providing restricted access to the data *values* whilst protecting the data *model*. This short customer story shows what that looks like for Data and Analytics teams in regulated industries.
|
||||
|
||||
The distinction is the whole point. An editor can add, correct and delete rows in the tables they are responsible for, but they cannot change the structure underneath - no new columns, no altered formats, no dropped keys. The shape of the table is fixed, and only its contents move. Every change is staged, validated at the point of entry, reviewed as a diff and approved by a data owner before it is loaded, with a full audit trail of who changed what, when and why.
|
||||
|
||||
See it in action:
|
||||
|
||||
<iframe title="Data Controller for SAS - a Customer Story" width="560" height="315" src="https://vid.4gl.io/videos/embed/kpSGoNE26FSif8X9A6pUfe" style="border: 0px;" allow="fullscreen" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe>
|
||||
|
||||
Would you like to see how this would work against your own tables? [Get in touch](/contact) to arrange a walkthrough.
|
||||
|
||||
<!--
|
||||
Source LinkedIn post:
|
||||
|
||||
Business users need to keep their data current. They do not need to change the tables underneath.
|
||||
|
||||
Most tools blur the two. Hand someone a spreadsheet and they reshape the file. Grant write access to a production table and they can reshape the table. Neither is what you want.
|
||||
|
||||
Data Controller for SAS® draws the line at the data VALUES. Business users self serve their updates - adding, correcting and deleting rows in the tables they own - with restricted access to what is inside the table, whilst the data MODEL stays protected. No new columns, no altered formats, no dropped keys.
|
||||
|
||||
→ Every change is staged and validated at the point of entry
|
||||
→ Reviewed as a diff and approved by a data owner before it loads
|
||||
→ With a full audit trail of who changed what, when and why
|
||||
|
||||
A short customer story, for Data and Analytics teams in regulated industries:
|
||||
|
||||
✅ Watch the story, then book a Discovery Call - link in the comments.
|
||||
|
||||
#sas #sasviya #datagovernance #datacapture #dataquality
|
||||
-->
|
||||
|
||||
<!-- Image prompt:
|
||||
|
||||
Generate a 16:9 landscape illustration for a B2B data-governance blog cover, 1200x627.
|
||||
|
||||
Style: flat vector illustration, dark slate background (#314351) with a faint evenly spaced dot grid, brand green accent (#90c445), lighter slate panels (#2e4252), soft diffused shadows, subtle depth.
|
||||
|
||||
Scene: a single data table drawn as two separated layers. The top layer is the table's structure - a header band of column blocks in muted slate, held behind a closed green padlock on the left. The bottom layer is the table's values - four rows of rounded cells in lighter slate, with two cells filled solid brand green and a small green pencil resting beside the lower right of the grid, as if those values are the ones being edited. A thin vertical gap separates the two layers, so the padlock reads as protecting the structure above while the open cells below are the editable values.
|
||||
|
||||
Minimal text, no letters, no numbers, no logos, no watermark. Keep the subject inside the central square so a 1:1 crop is safe; the outer left and right thirds may be croppable background only.
|
||||
|
||||
Output: save as ./customer-story.jpeg at 1200x627 (matching the other feed covers, and doubling as the LinkedIn share card). Do not also embed the image in the markdown body.
|
||||
-->
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: 'Every SAS® Project Needs to Load Data from Excel'
|
||||
description: After years of SAS consulting, one pattern emerged - every project needed Excel data loads, and every project built them from scratch. Here is how that journey led to Data Controller.
|
||||
date: '2026-08-17 19:15:00'
|
||||
author: 'Allan Bowe'
|
||||
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './sasprojects.jpeg'
|
||||
---
|
||||
|
||||
After several years of SAS consulting I noticed EVERY SINGLE PROJECT had a need to load data from Excel.
|
||||
|
||||
And EVERY SINGLE PROJECT would require dedicated SAS code (or ETL jobs) to read that data, cleanse, append to target, and to run in a secure production environment under batch credentials. This meant a dev/test/accept(+signoff)/prod lifecycle.
|
||||
|
||||
This process could literally take weeks.
|
||||
|
||||
As projects went by, I crafted a generic data capture tool using VBA, Stored Processes, and a shared network drive to share CSVs with SAS. It was a ZERO CODE solution that worked really well, and allowed ANY table to be configured so that business users could make changes in Excel and upload directly to production (with approval step).
|
||||
|
||||
It was a godsend.
|
||||
|
||||
Still, it had major drawbacks - users had to self-pull the latest version, and we still had this insecure network drive situation.
|
||||
|
||||
By 2020 the codebase was completely re-written as a SASjs Powered Web Application. The shared network drive was gone. The browser always has the latest version. We had SAS Viya compatibility and a growing number of satisfied (paying) customers.
|
||||
|
||||
By 2022 we had advanced Data Quality features, support for all flavours of SAS (Base, Meta, Viya), special SAS missing numerics and the ability to update formats in catalogs. Big-name customers in Banking, Insurance, Government, Travel, Pharma and other sectors.
|
||||
|
||||
In 2024 we had support for complex excel uploads (extract cells / ranges from anywhere in a workbook), detailed data audit, and a data rollback feature.
|
||||
|
||||
In 2026 we have a streamlined and tightened Viya deploy and many more validations - such as regex, and soon, frontend formulae.
|
||||
|
||||
The need to load ad-hoc data into SAS isn't going away.
|
||||
|
||||
Our mission is to make that process simple, secure, and fast.
|
||||
|
||||
To see how easily Data Controller can be deployed in your organisation, book a [Discovery Call](https://datacontroller.io/contact/) - or start today with the free [Community version](https://datacontroller.io/pricing/).
|
||||
|
||||
<!--
|
||||
Source LinkedIn post:
|
||||
|
||||
After several years #SAS consulting I noticed EVERY SINGLE PROJECT had a need to load data from Excel.
|
||||
|
||||
And EVERY SINGLE PROJECT would require dedicated SAS code (or ETL jobs) to read that data, cleanse, append to target, and to run in a secure production environment under batch credentials. This meant a dev/test/accept(+signoff)/prod lifecycle.
|
||||
|
||||
This process could literally take weeks.
|
||||
|
||||
As projects went by, I crafted a generic data capture tool using VBA, Stored Processes, and a shared network drive to share CSVs with SAS. It was a ZERO CODE solution that worked really well, and allowed ANY table to be configured so that business users could make changes in Excel and upload directly to production (with approval step).
|
||||
|
||||
It was a godsend.
|
||||
|
||||
Still, it had major drawbacks - users had to self-pull the latest version, and we still had this insecure network drive situation.
|
||||
|
||||
By 2020 the codebase was completely re-written as a SASjs Powered Web Application. The shared network drive was gone. The browser always has the latest version. We had #SASViya compatibility and a growing number of satisfied (paying) customers.
|
||||
|
||||
By 2022 we had advanced Data Quality features, support for all flavours of SAS (Base, Meta, Viya), special SAS missing numerics and the ability to update formats in catalogs. Big-name customers in Banking, Insurance, Government, Travel, Pharma and other sectors.
|
||||
|
||||
In 2024 we had support for complex excel uploads (extract cells / ranges from anywhere in a workbook), detailed data audit, and a data rollback feature.
|
||||
|
||||
In 2026 we have a streamlined and tightened Viya deploy and many more validations - such as regex, and soon, frontend formulae.
|
||||
|
||||
The need to load ad-hoc data into SAS isn't going away.
|
||||
|
||||
Our mission is to make that process simple, secure, and fast.
|
||||
|
||||
To see how easily Data Controller can be deployed in your organisation, book a Discovery Call (link in comments) - or start today with the free Community version.
|
||||
|
||||
#datamanagement
|
||||
|
||||
Image prompt: A flat-style illustration showing an Excel spreadsheet with an arrow flowing into a secure database server, with a subtle timeline (VBA era to modern web app) in the background, blue and green corporate palette, clean minimal design suitable for a LinkedIn/blog cover image.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 582 KiB |
|
After Width: | Height: | Size: 473 KiB |
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: 'The 5 Lines of Defence for Business Data Inputs'
|
||||
description: How Data Controller for SAS® defends data quality for workflows deriving from business inputs - data model, permissions, validation checks, post edit hooks, and approvals.
|
||||
date: '2026-08-27 09:00:00'
|
||||
author: 'Allan Bowe'
|
||||
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './five-lines-of-defence.jpeg'
|
||||
---
|
||||
|
||||
How does Data Controller for SAS® defend data quality for data workflows deriving from business inputs?
|
||||
|
||||
**⚔️ 1st Line of Defence - Data Model ⚔️**
|
||||
|
||||
The interface will only accept inputs conforming to this model (columns, types, lengths, indexes, constraints etc). The schema determines the behaviour - eg, a date format results in a date picker.
|
||||
|
||||
**⚔️ 2nd Line of Defence - Data Permissions ⚔️**
|
||||
|
||||
Changes are made using a SAS System Account (eg `sassrv`). This means you can safely DENY write-access to data for business users, preventing un-controlled data ingestion.
|
||||
|
||||
**⚔️ 3rd Line of Defence - Validation Checks ⚔️**
|
||||
|
||||
Additional validation checks (eg value ranges, specific patterns) can be configured to run at the point of data capture, prior to SAS upload. See the [validation documentation](https://docs.datacontroller.io/dcc-validations/) for details.
|
||||
|
||||
**⚔️ 4th Line of Defence - Post Edit Hook ⚔️**
|
||||
|
||||
Complex / customer specific validation can be deployed as SAS code, to run after every EDIT (prior to APPROVAL) using a [post edit hook](https://docs.datacontroller.io/dcc-tables/#post_edit_hook). A failure here results in immediate user feedback / change rejection.
|
||||
|
||||
**⚔️ 5th Line of Defence - Approval Step(s) ⚔️**
|
||||
|
||||
Changes are reviewed with [one or more approvals](https://docs.datacontroller.io/dcc-tables/) BEFORE being applied to the target database. A full audit trail is also maintained.
|
||||
|
||||
All functionality is ZERO-CODE, works on SAS Viya / EBI / SASjs Server, and applies to any database you have an ACCESS engine for. There is also a [community edition](https://datacontroller.io/pricing/) - free for unlimited users.
|
||||
|
||||
If you'd like to strengthen your own defences - [let's chat](https://datacontroller.io/contact/).
|
||||
|
||||
<!--
|
||||
Source LinkedIn post:
|
||||
|
||||
How does Data Controller for SAS® defend #DataQuality for #DataWorkflows deriving from Business Inputs?
|
||||
|
||||
⚔️ 1st Line of Defence - Data Model ⚔️
|
||||
The interface will only accept inputs conforming to this model (columns, types, lengths, indexes, constraints etc). The schema determines the behaviour - eg, a date format results in a date picker.
|
||||
|
||||
⚔️ 2nd Line of Defence - Data Permissions ⚔️
|
||||
Changes are made using a SAS System Account (eg `sassrv`). This means you can safely DENY write-access to data for business users, preventing un-controlled #DataIngestion.
|
||||
|
||||
⚔️ 3rd Line of Defence - Validation checks ⚔️
|
||||
Additional validation checks (eg value ranges, specific patterns) can be configured to run at the point of data capture, prior to #SAS upload. The docs for these are here: https://lnkd.in/djtaGPsr
|
||||
|
||||
⚔️ 4th Line of Defence - Post Edit Hook ⚔️
|
||||
Complex / customer specific validation can be deployed as SAS code, to run after every EDIT (prior to APPROVAL). A failure here results in immediate user feedback / change rejection.
|
||||
|
||||
⚔️ 5th Line of Defence - Approval Step(s) ⚔️
|
||||
Changes are reviewed with one or more approvals BEFORE being applied to the target database. A full audit trail is also maintained.
|
||||
|
||||
All functionality is ZERO-CODE, works on #sasViya / EBI / SASjs Server, and applies to any database you have an ACCESS engine for. There is also a Community edition - free for unlimited users.
|
||||
|
||||
#datagovernance #sasadmin #saspartners
|
||||
-->
|
||||
|
||||
<!-- Image prompt:
|
||||
A flat-design illustration in the Data Controller brand style (dark navy background, teal/green and orange accents), composed around a SINGLE CENTRAL focal point so it survives a square crop: a medieval-castle motif reimagined as a data fortress - five concentric defensive walls (rendered as clean glowing ring segments, each a slightly different teal/green shade, subtly numbered 1-5) surrounding a central database cylinder that glows safely at the core. An arrow or data-packet stream approaches from the top, passing checkpoints in each wall: a schema/grid icon (wall 1), a padlock (wall 2), a checklist with ticks (wall 3), a code angle-brackets icon (wall 4), and a stamp/approval tick (wall 5). One red/orange invalid packet is shown bouncing off an outer wall. All key elements within the central square of the frame; outer left and right thirds contain only background texture and glow, safe to crop. Minimal text, no logos. Professional but lightly playful, suitable for a B2B data product site. 16:9 landscape, 1200x627, suitable as a blog/feed cover image.
|
||||
|
||||
Generated with: local Routstr node (see blog-dev/skills routstr-image-generation), model gemini-3.1-flash-lite-image, output 1424x736 JPEG, ~31 sats.
|
||||
-->
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
title: 'FSEDIT-Style Paging in the Edit Record Form'
|
||||
description: Page through your data one record at a time in the Edit Record dialog - a modern take on the classic SAS PROC FSEDIT full-screen editor.
|
||||
date: '2026-07-30 09:00:00'
|
||||
date: '2026-08-09 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data_controller
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './example.png'
|
||||
@@ -11,7 +11,7 @@ previewImg: './example.png'
|
||||
|
||||
# FSEDIT-Style Paging in the Edit Record Form
|
||||
|
||||
Anyone who has worked with SAS for a while will remember `PROC FSEDIT`. Part of the SAS/FSP (Full-Screen Product) family alongside `PROC FSVIEW` and `PROC FSBROWSE`, it was for years the standard way to edit a dataset interactively: one observation per screen, typing directly over values that committed straight to the table. That focused, one-record-at-a-time view was a simple way to amend data without getting lost in a wide grid - though edits landed immediately, with no staging or approval step in between.
|
||||
Anyone who has worked with SAS for a while will remember `PROC FSEDIT`. Part of the SAS/FSP (Full-Screen Product) family alongside `PROC FSVIEW` and `PROC FSBROWSE`, it was for years the standard way to edit a dataset interactively: one observation per screen, typing directly over values, and commiting straight to the underlying dataset. That focused, one-record-at-a-time view was a simple way to amend data without getting lost in a wide grid - though edits landed immediately, with no staging or approval step in between.
|
||||
|
||||
Data Controller's **Edit Record** dialog brings that experience into the browser. Instead of scanning across dozens of columns in the grid, you open a single row as a form - every field labelled and stacked vertically, with dropdowns, validations and formatting applied just as they are in the main editor.
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: 'Full Table Search: Find Any Value in Any Table'
|
||||
description: Type a value into the search box and Data Controller scans every column for it - no query, no WHERE clause.
|
||||
date: '2026-09-15 17:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './full-table-search.jpeg'
|
||||
---
|
||||
|
||||
# Full Table Search: Find Any Value in Any Table
|
||||
|
||||
Finding a value in a large table usually means writing a query first: guess which column it lives in, write a WHERE clause, run it, and try again when you guess wrong. Data Controller's **full table search** removes that step. Choose a library and table in the Viewer, type a value into the search box and press Enter - every column in the table is scanned for it, and the matching rows come straight back into the grid.
|
||||
|
||||
## How it works
|
||||
|
||||
The search box sits in the Viewer toolbar, next to a **Numeric** checkbox:
|
||||
|
||||
- **Text search** matches part of a value using the case sensitive SAS® `CONTAINS` operator, so `smith` finds `Goldsmith` but not `Smithson`.
|
||||
- **Numeric search** (tick the box) matches the number exactly against every numeric column in the table.
|
||||
|
||||
Whichever you use, the results are ordinary rows in the Viewer, with the rest of the screen behaving exactly as it does for a normal view.
|
||||
|
||||
## Any database, not just SAS datasets
|
||||
|
||||
The scan runs inside SAS, against whichever libname engine the table is assigned to - so it is not limited to SAS datasets.
|
||||
|
||||
## Built on open source
|
||||
|
||||
This feature, like most of Data Controller, is built on the [SASjs Macro Core](https://github.com/sasjs/core) library, using the [`%mp_searchdata()`](https://core.sasjs.io/mp__searchdata_8sas.html) macro. The macro assembles a single DATA step with one `OR` clause per column - a `CONTAINS` test for character columns, an equality test for numeric ones - and writes out only the matching records. It is MIT licensed, so you can read, test and audit the code that is running against your data.
|
||||
|
||||
A few things worth knowing:
|
||||
|
||||
- The search respects your current filter, Row Level Security and Column Level Security - it scans the view you are entitled to see, not the raw table.
|
||||
- Full table search is available in ViewBoxes as well, so the related tables lined up beside your main grid can be searched the same way.
|
||||
- Nothing is shipped to an external index or search service: the scan happens on your own SAS platform.
|
||||
|
||||
See it in action:
|
||||
|
||||
<iframe title="Full Table Search" width="560" height="315" src="https://vid.4gl.io/videos/embed/qdEv4PP2oiPr5VXwLbN58B" style="border: 0px;" allow="fullscreen" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe>
|
||||
|
||||
More detail in the [Viewer documentation](https://docs.datacontroller.io/dcu-tableviewer/).
|
||||
|
||||
<!--
|
||||
Source LinkedIn post:
|
||||
|
||||
Data Controller for SAS® has a plethora of features to make Data Discovery easier
|
||||
|
||||
Here we demonstrate "full table search". No need to formulate a query - just type a value and hit enter!
|
||||
|
||||
It works on all databases
|
||||
|
||||
This feature, like most others, is built on our open-source #SASjs library - using the `mp_searchdata()` macro (https://lnkd.in/gxZZq76j).
|
||||
|
||||
#sas #sasapps #sasviya
|
||||
|
||||
video: https://vid.4gl.io/w/qdEv4PP2oiPr5VXwLbN58B
|
||||
-->
|
||||
|
||||
<!-- Image prompt:
|
||||
Flat vector illustration on a dark slate (#314351) background with a faint dot grid: a stylised data table across the lower half (rounded header bar plus eight rows of rounded cells), with the fourth row highlighted in brand green (#90c445). A large magnifying glass with a thick green rim and diagonal handle overlaps the table on the right, and inside its dark lens the highlighted row appears magnified with two white value blocks. Headline "FULL TABLE SEARCH" in bold white uppercase, subheading "any value, every column - no query required" in brand green. 1200x627 landscape, no other text. Composed programmatically with PIL in the Data Controller brand palette rather than generated by an image model.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 436 KiB |
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: 'Some of You Will "Git" This'
|
||||
description: Pull requests, diffs, hooks, tests and pipelines - for your production data. Data Controller brings the git workflow to master data management in SAS.
|
||||
date: '2026-08-13 14:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './gitflow.png'
|
||||
---
|
||||
|
||||
# Some of You Will "Git" This
|
||||
|
||||
If you could make a "pull request" for production data, how might that look?
|
||||
|
||||
Perhaps:
|
||||
|
||||
- ✅ A [before / after comparison](https://docs.datacontroller.io/dc-userguide/) of the values that had changed
|
||||
- ✅ [Security](https://docs.datacontroller.io/dcc-security/) (who can submit / approve in each "repo")
|
||||
- ✅ Only store the changed data (not full copies)
|
||||
- ✅ A new "diff" when the base table changes
|
||||
- ✅ Ability to choose [one or more approvers](https://docs.datacontroller.io/dcc-tables/)
|
||||
- ✅ [HOOK scripts](https://docs.datacontroller.io/dcc-tables/) before / after a commit
|
||||
- ✅ Tests ([DQ checks](https://docs.datacontroller.io/dcc-validations/)) prior to merge
|
||||
- ✅ A [pipeline](https://docs.datacontroller.io/dcc-tables/) following a merge
|
||||
- ✅ [Emails](https://docs.datacontroller.io/emails/) for "status" changes
|
||||
- ✅ Comments by reviewers
|
||||
- ✅ Full [history](https://docs.datacontroller.io/dc-userguide/) of changes
|
||||
|
||||
Sound familiar? This is exactly how developers have managed *code* for decades - and Data Controller brings the same discipline to *data*. Every edit to a table in SAS® goes through an edit - stage - approve workflow: changes are staged as a diff, checked by validation rules and hook scripts, reviewed and approved by the people you choose, then loaded. The audit trail captures who changed what, when and why - the `git blame` of master data.
|
||||
|
||||
If you're ready to "commit" to Data Control, we invite you to "clone" the latest version of [Data Controller for SAS®](https://datacontroller.io) - which has ALL these features - and "push" to your "main" environment for some "master" data management!
|
||||
|
||||
<!-- Source LinkedIn post:
|
||||
Some of you will "git" this..
|
||||
|
||||
If you could make a "pull request" for production data, how might that look?
|
||||
|
||||
Perhaps:
|
||||
|
||||
✅ A before / after comparison of the values that had changed
|
||||
✅ Security (who can submit / approve in each "repo")
|
||||
✅ Only store the changed data (not full copies)
|
||||
✅ A new "diff" when the base table changes
|
||||
✅ Ability to choose one or more approvers
|
||||
✅ HOOK scripts before / after a commit
|
||||
✅ Tests (DQ checks) prior to merge
|
||||
✅ A pipeline following a merge
|
||||
✅ Emails for "status" changes
|
||||
✅ Comments by reviewers
|
||||
✅ Full history of changes
|
||||
|
||||
Sound familiar? This is exactly how developers have managed CODE for decades - and Data Controller brings the same discipline to DATA.
|
||||
|
||||
Every edit to a table in Data Controller for SAS® goes through an edit - stage - approve workflow: changes are staged as a diff, checked by validation rules and hook scripts, reviewed and approved by the people you choose, then loaded.
|
||||
|
||||
The audit trail captures who changed what, when and why - the "git blame" of master data.
|
||||
|
||||
If you're ready to "commit" to Data Control, we invite you to "clone" the latest version of Data Controller for SAS - which has ALL these features - and "push" to your "main" environment for some "master" data management!
|
||||
|
||||
Link in the comments below 👇
|
||||
|
||||
#dataops #mdm #masterdata #dataquality #sas #sasjs #datagovernance
|
||||
-->
|
||||
|
||||
<!-- Image prompt:
|
||||
A clean, flat-design illustration in the Data Controller brand style showing a database table flowing through a git-style pull-request workflow: rows of a data table on the left passing through a "stage" step shown as a highlighted before/after diff of changed cells, then an approval checkpoint with a tick and reviewer avatars, then merging into a production database cylinder on the right. Git-flavoured touches: a branch line diagram arcing from "staging" branch into "main", small commit dots along the line. Dark navy background, teal/green accents, orange highlights on the changed cells. Minimal text: "stage -> approve -> merge". 16:9 landscape, suitable as a blog/feed cover image.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 512 KiB |
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: 'Horror Stories of Business Data Ingestion'
|
||||
description: Real-world tales of business data ingestion gone wrong - from broken ETL jobs to network-share "audit trails" and the Access database under the operations desk.
|
||||
date: '2026-08-15 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
previewImg: './horror.jpeg'
|
||||
tags:
|
||||
- Master Data Management
|
||||
---
|
||||
|
||||
# Horror Stories of Business Data Ingestion
|
||||
|
||||
> "We spent 6 weeks recruiting a Data Engineer, who built several complex ETL workflows, which promptly broke on his departure thanks to a minor change in the source file"
|
||||
|
||||
> "I wrote the SQL update script myself, which was working fine until my holiday cover adjusted the where clause, and wiped half the records. At least, that's what I think happened. We didn't see the discrepancy until the following month."
|
||||
|
||||
> "We often need to upload new mapping files and so all the Business Analysts in the Customer Insights team are being trained on the enterprise ETL tool"
|
||||
|
||||
> "Our DBA does the manual updates, but he only has 0.5 free days per month allocated to the task - and he will only do it AFTER month end"
|
||||
|
||||
> "We have a dedicated data engineering team and they can knock out an ETL job in a few minutes. The problem is that it takes 4-8 hours of documentation and meetings to set the requirements, and 4-8 weeks to deploy the job into production"
|
||||
|
||||
> "If you need to change the data values, just double click the table in our .NET application and type it in. Remember to close it afterwards though, else the overnight batch job will fail due to a physical lock. This happens quite a lot - it's safest to just close the application afterwards and re-open it."
|
||||
|
||||
> "We keep an audit trail of ALL manual updates. Whoever makes the change will just open the spreadsheet, fill in the necessary records, and save the file with a new version number on the department network share"
|
||||
|
||||
> "We don't have any business data ingestion, we only load source data, and it is never modified"
|
||||
|
||||
> "We export the data from the web report into Excel. We copy/paste the data into another Excel sheet where we execute some VBscript macros that were developed by a guy who left the company. After that, the updated data are saved as a csv file on a network drive. The file is then picked up by a batch job and loaded into the database. The job is scheduled to run every 15 minutes. The updated records can then be displayed in our reports." - [Xavier Bizoux](https://www.linkedin.com/in/xavierbizoux) ([source](https://www.linkedin.com/feed/update/urn:li:activity:7183406509450547200?commentUrn=urn%3Ali%3Acomment%3A%28activity%3A7183406509450547200%2C7184815374624772096%29&dashCommentUrn=urn%3Ali%3Afsd_comment%3A%287184815374624772096%2Curn%3Ali%3Aactivity%3A7183406509450547200%29))
|
||||
|
||||
Coupled with:
|
||||
|
||||
> "We don't load our data to the DataWarehouse, it's too difficult. We just stick it all in the team ACCESS database on the PC under the operations desk"
|
||||
|
||||
What are YOUR horror stories?
|
||||
|
||||
<!--
|
||||
Source LinkedIn post:
|
||||
|
||||
Horror Stories of Business Data Ingestion
|
||||
|
||||
"We spent 6 weeks recruiting a Data Engineer, who built several complex ETL workflows, which promptly broke on his departure thanks to a minor change in the source file"
|
||||
|
||||
"I wrote the SQL update script myself, which was working fine until my holiday cover adjusted the where clause, and wiped half the records. At least, that's what I think happened. We didn't see the discrepancy until the following month."
|
||||
|
||||
"We often need to upload new mapping files and so all the Business Analysts in the Customer Insights team are being trained on the enterprise ETL tool"
|
||||
|
||||
"Our DBA does the manual updates, but he only has 0.5 free days per month allocated to the task - and he will only do it AFTER month end"
|
||||
|
||||
"We have a dedicated data engineering team and they can knock out an ETL job in a few minutes. The problem is that it takes 4-8 hours of documentation and meetings to set the requirements, and 4-8 weeks to deploy the job into production"
|
||||
|
||||
"If you need to change the data values, just double click the table in our .NET application and type it in. Remember to close it afterwards though, else the overnight batch job will fail due to a physical lock. This happens quite a lot - it's safest to just close the application afterwards and re-open it."
|
||||
|
||||
"We keep an audit trail of ALL manual updates. Whoever makes the change will just open the spreadsheet, fill in the necessary records, and save the file with a new version number on the department network share"
|
||||
|
||||
"We don't have any business data ingestion, we only load source data, and it is never modified"
|
||||
|
||||
"We export the data from the web report into Excel. We copy/paste the data into another Excel sheet where we execute some VBscript macros that were developed by a guy who left the company. After that, the updated data are saved as a csv file on a network drive. The file is then picked up by a batch job and loaded into the database. The job is scheduled to run every 15 minutes. The updated records can then be displayed in our reports." - Xavier Bizoux (https://www.linkedin.com/in/xavierbizoux)
|
||||
Source comment: https://www.linkedin.com/feed/update/urn:li:activity:7183406509450547200?commentUrn=urn%3Ali%3Acomment%3A%28activity%3A7183406509450547200%2C7184815374624772096%29&dashCommentUrn=urn%3Ali%3Afsd_comment%3A%287184815374624772096%2Curn%3Ali%3Aactivity%3A7183406509450547200%29
|
||||
|
||||
Coupled with:
|
||||
|
||||
"We don't load our data to the DataWarehouse, it's too difficult. We just stick it all in the team ACCESS database on the PC under the operations desk"
|
||||
|
||||
What are YOUR horror stories?
|
||||
|
||||
#masterdatamanagement #dataops #datagovernance
|
||||
-->
|
||||
|
||||
<!-- Image prompt:
|
||||
A spooky-but-professional flat-design illustration in the Data Controller brand style for a "Horror Stories of Business Data Ingestion" post: a haunted-house style scene rendered in dark navy, with ghostly spreadsheets, a broken ETL pipeline (cracked pipe segments leaking data rows), an old Microsoft Access database icon lurking under a desk, and a graveyard of tombstones labelled with file versions like "mapping_v12_FINAL_v2.xlsx". A full moon and a few bats in the background. Teal/green and orange accents for the eerie glow on the data elements. Humorous, Halloween-flavoured but still suitable for a B2B data product site. Minimal text. 16:9 landscape, suitable as a blog/feed cover image.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 51 KiB |
@@ -0,0 +1,76 @@
|
||||
---
|
||||
title: 'If: An Ode to Data Control'
|
||||
description: Rudyard Kipling's 'If', reworked for the data governance era - the full text of a poem first published in 2022.
|
||||
date: '2026-09-20 15:00:00'
|
||||
author: 'Allan Bowe'
|
||||
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './if-an-ode-to-data-control.jpeg'
|
||||
---
|
||||
|
||||
# If: An Ode to Data Control
|
||||
|
||||
Rudyard Kipling wrote "If" in 1895 and published it in his 1910 collection Rewards and Fairies. This ode to data control was first published in 2022. Here is the full text.
|
||||
|
||||
```poem
|
||||
If you can keep your data when all about you
|
||||
Are losing theirs and blaming it on you;
|
||||
If you can trust your metrics when analysts doubt you,
|
||||
But make allowance for their doubting too:
|
||||
|
||||
If you can query and not be tired by waiting,
|
||||
Or building models, adjust for outliers,
|
||||
Define ETL modules that are self-validating,
|
||||
And require contracts from data suppliers;
|
||||
|
||||
|
||||
If you can report - and not make KPIs your master;
|
||||
If you can forecast - and not make compliance your aim,
|
||||
If you can make the overnight batch faster
|
||||
And ensure the end results are just the same:
|
||||
|
||||
If you can't bear two versions of truth spoken
|
||||
Produced by silos to make a trap for fools,
|
||||
Or watch systems you gave your life to, broken,
|
||||
And stoop and build 'em up with off-the-shelf tools;
|
||||
|
||||
|
||||
If you would make one lake with all your data
|
||||
And risk it on one vendor's big bang plan,
|
||||
And lose, and start again at invitation-to-tender
|
||||
And salvage from the project, what you can:
|
||||
|
||||
If you can force your flat files and mainframe
|
||||
To serve datamarts when key DBAs retire,
|
||||
And so hold on when stakeholders proclaim
|
||||
Why (oh why) is our Data Quality so dire?
|
||||
|
||||
|
||||
If you can talk with architects & keep your virtue,
|
||||
Or walk with CEOs - nor lose the accounting touch,
|
||||
If neither UTF-8 nor PII can hurt you,
|
||||
If all teams count with you, but none too much:
|
||||
|
||||
If you can keep Data Owners beholden
|
||||
To uploads that are timely and accurate and whole,
|
||||
Yours is the Earth and clean records (golden),
|
||||
And - which is more - you'll have Data Control!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*If you can't keep your data when all about you are losing theirs - [Data Controller for SAS](https://datacontroller.io) can help. Capture, review and approval for every change, with a full audit trail, on SAS Viya, SAS 9 EBI and SASjs Server.*
|
||||
|
||||
<!--
|
||||
Source LinkedIn post:
|
||||
|
||||
If you can keep your data when all about you
|
||||
are losing theirs, and blaming it on you...
|
||||
|
||||
#datagovernance #dataquality #masterdatamanagement
|
||||
-->
|
||||
|
||||
<!-- Image prompt:
|
||||
Classic book title-page cover on a dark slate (#314351) background with a faint vignette and dot grid. A centred cream paper card with a drop shadow, double rule and green spine hint holds the title page: "FIRST PUBLISHED 2022" in small caps, green ornament rules, "IF" in large bold serif, "An Ode to Data Control" in serif, "after Rudyard Kipling" in italic, and the closing couplet "And - which is more - you'll have Data Control!" above a green rule. 1200x627 landscape, composed programmatically with PIL in the Data Controller brand palette rather than generated by an image model.
|
||||
-->
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: 'Struggling with Legacy Data Ingestion?'
|
||||
description: Filename conventions, network drives, ancient formats and 5am batch failures - legacy spreadsheet ingestion hurts everyone. With Data Controller for SAS®, those issues disappear.
|
||||
date: '2026-08-24 09:00:00'
|
||||
author: 'Allan Bowe'
|
||||
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './legacy-data-ingestion.jpeg'
|
||||
---
|
||||
|
||||
Do you struggle against legacy data ingestion processes?
|
||||
|
||||
Are your analysts saving spreadsheets with a particular filename, on a particular network drive, on a particular day, with a specific structure?
|
||||
|
||||
Perhaps your process necessitates an ancient format (.xls, Excel '95) or particular encoding.
|
||||
|
||||
Sucks, right?
|
||||
|
||||
It sucks also for the Platform Manager, who gets the email at 5am when the batch breaks due to something not-quite-right in said input file.
|
||||
|
||||
Or the Data Engineer, spending all day knocking out generic excel import routines. Or the sponsor, paying for a broken process, that should take seconds.
|
||||
|
||||
With Data Controller for SAS®, all these issues... disappear.
|
||||
|
||||
Thanks to our OEM licence of [SheetJS](https://sheetjs.com), analysts can self-serve data uploads via the browser - no (insecure) network drive needed, nor thick client install.
|
||||
|
||||
And the data really can be **anywhere** in the workbook - in any shape, spread across any number of disparate sheets, ranges and cells. A range of [import options](https://docs.datacontroller.io/excel/) handles everything from clean tabular sheets to sprawling financial reports.
|
||||
|
||||
Once automatic validations and DQ checks pass, and approval received, the target table is updated and any onward jobs are triggered. Needless to say, there is a full audit trail right back to the original excel file.
|
||||
|
||||
Not only that - you can load ANY database, SAS dataset, format, or CAS table. New data targets are configured by the admin - zero code, nothing to deploy or drag through to production.
|
||||
|
||||
There is also a [community edition](https://datacontroller.io/pricing/), free for unlimited users (restricted by rows).
|
||||
|
||||
If you'd like to excel by spending less time on Excel - [let's chat](https://datacontroller.io/contact/).
|
||||
|
||||
<!--
|
||||
Source LinkedIn post:
|
||||
|
||||
Do you struggle against legacy data ingestion processes?
|
||||
|
||||
Are your analysts saving spreadsheets with a particular filename, on a particular network drive, on a particular day, with a specific structure?
|
||||
|
||||
Perhaps your process necessitates an ancient format (.xls, Excel '95) or particular encoding.
|
||||
|
||||
Sucks, right?
|
||||
|
||||
It sucks also for the Platform Manager, who gets the email at 5am when the batch breaks due to something not-quite-right in said input file.
|
||||
|
||||
Or the Data Engineer, spending all day knocking out generic excel import routines. Or the sponsor, paying for a broken process, that should take seconds.
|
||||
|
||||
With Data Controller for SAS®, all these issues... disappear.
|
||||
|
||||
Thanks to our OEM licence of SheetJS, analysts can self-serve data uploads via the browser - no (insecure) network drive needed, nor thick client install.
|
||||
|
||||
The data can be anywhere in the workbook - in any shape, across any number of disparate sheets, ranges and cells - thanks to a range of import options. Columns in any order. Additional columns (not in the target table) are simply ignored. Invalid columns cause immediate rejection, so the uploader can rectify.
|
||||
|
||||
Once automatic validations and DQ checks pass, and approval received, the target table is updated and any onward jobs are triggered. Needless to say, there is a full audit trail right back to the original excel file.
|
||||
|
||||
Not only that - you can load ANY database, #SAS dataset or CAS table. New data targets are configured by the admin - zero code, nothing to deploy or drag through to production.
|
||||
|
||||
There is also a community edition, free for unlimited users (restricted by rows).
|
||||
|
||||
If you'd like to excel by spending less time on Excel - let's chat.
|
||||
|
||||
#datamanagement #excel #datagovernance
|
||||
-->
|
||||
|
||||
<!-- Image prompt:
|
||||
A flat-design illustration in the Data Controller brand style (dark navy background, teal/green and orange accents), composed around a SINGLE CENTRAL focal point so it survives a square crop: in the centre, a large glowing browser window with a spreadsheet file being dropped into it and a green tick, feeding a short downward flow into a database cylinder directly beneath. Sinking into shadow at the very bottom of the frame, small and half-buried, the legacy relics being left behind: a floppy disk labelled .xls, a dusty network drive wrapped in chains, and an alarm clock showing 5am. Light radiates from the central browser window against the dark background. All key elements within the central square of the frame; outer left and right thirds contain only background texture and glow, safe to crop. Minimal text, no logos. Professional but lightly humorous, suitable for a B2B data product site. 16:9 landscape, 1200x627, suitable as a blog/feed cover image.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 370 KiB |
|
After Width: | Height: | Size: 53 KiB |
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: 'SAS-isms: Library Info'
|
||||
description: Expanding a VIEW library in Data Controller surfaces engine, size, paths, table count and schema - with the same data queryable from the underlying tables.
|
||||
date: '2026-08-11 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- SAS-isms
|
||||
previewImg: './library-info.jpeg'
|
||||
---
|
||||
|
||||
# Library Info
|
||||
|
||||
One of many "SAS-isms" in Data Controller for SAS®.
|
||||
|
||||
When expanding a VIEW library, Data Catalog items are surfaced:
|
||||
|
||||
- Library Engine
|
||||
- Library Size (Bytes, MB, GB)
|
||||
- Paths (if BASE engine)
|
||||
- Table Count
|
||||
- Schema (if a database)
|
||||
|
||||

|
||||
|
||||
And more. Much of this data can also be queried from the underlying tables:
|
||||
|
||||
- [MPE_DATACATALOG_LIBS](https://docs.datacontroller.io/tables/mpe_datacatalog_libs) - engine, paths, permissions, owners, schemas, metadata name / id
|
||||
- [MPE_DATASTATUS_LIBS](https://docs.datacontroller.io/tables/mpe_datastatus_libs) - frequently changing attributes: library size, table count, catalog count
|
||||
|
||||
The split is deliberate - stable attributes live in the DATACATALOG tables, volatile ones (size, counts) in DATASTATUS. All of it is stored with SCD2, so you can track how your data landscape changes over time.
|
||||
|
||||
Refreshes are driven by the `mpe_refreshlibs` and `mpe_refreshtables` macros - any user can refresh an individual library from the VIEW menu, and admins can scan the entire estate from the System page.
|
||||
|
||||
Similar information is available for datasets - it's essentially an extract from `proc contents`, using the [mp_dsmeta](https://core.sasjs.io/mp__dsmeta_8sas.html) macro from the [SASjs Core](https://github.com/sasjs/core) library.
|
||||
|
||||

|
||||
|
||||
<!--
|
||||
LinkedIn version (plain text - no markdown, links in comments):
|
||||
|
||||
One of many "SAS-isms" in Data Controller for SAS® - "Library Info"
|
||||
|
||||
When expanding a VIEW library, Data Catalog items are surfaced:
|
||||
|
||||
▪️ Library Engine
|
||||
▪️ Library Size (Bytes, MB, GB)
|
||||
▪️ Paths (if BASE engine)
|
||||
▪️ Table Count
|
||||
▪️ Schema (if a database)
|
||||
|
||||
And more. Much of this data can also be queried from the underlying tables (links in the comments below):
|
||||
|
||||
▪️ MPE_DATACATALOG_LIBS - engine, paths, permissions, owners, schemas, metadata name / id
|
||||
▪️ MPE_DATASTATUS_LIBS - frequently changing attributes: library size, table count, catalog count
|
||||
|
||||
The split is deliberate - stable attributes live in the DATACATALOG tables, volatile ones (size, counts) in DATASTATUS. All of it is stored with SCD2, so you can track how your data landscape changes over time.
|
||||
|
||||
Refreshes are driven by the mpe_refreshlibs and mpe_refreshtables macros - any user can refresh an individual library from the VIEW menu, and admins can scan the entire estate from the System page.
|
||||
|
||||
Similar information is available for datasets - it's essentially an extract from proc contents, using the mp_dsmeta macro from the SASjs Core library (link in the comments).
|
||||
|
||||
#sas #datacatalog #datagovernance #sasjs #metadata
|
||||
|
||||
Suggested first comment:
|
||||
|
||||
MPE_DATACATALOG_LIBS: https://docs.datacontroller.io/tables/mpe_datacatalog_libs
|
||||
MPE_DATASTATUS_LIBS: https://docs.datacontroller.io/tables/mpe_datastatus_libs
|
||||
mp_dsmeta macro: https://core.sasjs.io/mp__dsmeta_8sas.html
|
||||
|
||||
Attach library-info.jpeg (and optionally library-enginehost-info.jpeg + dataset-attributes.jpeg) to the post.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 754 KiB |
@@ -0,0 +1,85 @@
|
||||
---
|
||||
title: 'Oops! Now You Can Roll Back Data Changes'
|
||||
description: Despite all the checks in Data Controller for SAS®, sometimes the wrong updates get approved. You can now roll back to a previous version - with full audit history preserved.
|
||||
date: '2026-08-20 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './ctrl-z.jpeg'
|
||||
---
|
||||
|
||||
# Oops! Now You Can Roll Back Data Changes
|
||||
|
||||
Despite the MANY checks and guarantees in Data Controller for SAS®, sometimes it can happen that the wrong updates are approved and applied.
|
||||
|
||||
Thankfully, it is now possible to **roll back** data changes to a previous state.
|
||||
|
||||

|
||||
|
||||
## How it works in practice
|
||||
|
||||
Behind the scenes, rollback is not a silent undo. It is a **first-class approval workflow** just like any other edit. When you choose to restore a previous version, the backend - via the [`%mp_stripdiffs`](https://core.sasjs.io/mp__stripdiffs_8sas.html) macro - reads the `MPE_AUDIT` table (or a custom `AUDIT_LIBDS` configured for the table) and computes every difference between the current state and the version you want to go back to.
|
||||
|
||||
It handles all three change types:
|
||||
|
||||
- **Deleted rows** are re-inserted with their original values.
|
||||
- **Modified rows** are reverted to their previous values.
|
||||
- **Added rows** are marked for deletion with `_____DELETE__THIS__RECORD_____="Yes"`.
|
||||
|
||||
The computed differences are written to a new staging package in the approvals directory, complete with a CSV and a `macvars.sas` snapshot of the session context. A new `LOAD_REF` is generated, and the package is submitted via the standard `%mpe_loader` service - so it goes through the same edit-stage-approve workflow as any manual change.
|
||||
|
||||
This means the rollback itself is **reviewable and approvable**. Nothing is applied silently, and the full audit trail is maintained: the reversion appears as a new load reference in `MPE_SUBMIT`, `MPE_REVIEW`, `MPE_DATALOADS`, and `MPE_AUDIT`, just like any other submission.
|
||||
|
||||
## Security and access
|
||||
|
||||
Not everyone can roll back everything. The `%mpe_checkrestore` macro enforces a strict access check before the restore service will run:
|
||||
|
||||
- The load must actually exist and have been loaded (no rollbacks of unapproved submissions).
|
||||
- The table must be configured with an audit table.
|
||||
- The user must have `EDIT` access to the target table.
|
||||
- If the user is not an admin, Row Level Security or Column Level Security rules on the table will block the restore.
|
||||
|
||||
If access is denied, the service aborts with a clear reason - no opaque errors.
|
||||
|
||||
## What this means for compliance
|
||||
|
||||
Because the rollback creates a new, approved changeset rather than silently rewinding history, auditors can see exactly what was reverted, when, and by whom. The `MPE_AUDIT` table retains the record of every intermediate state, so nothing is ever truly lost. For tables where data integrity is critical - regulatory reporting, actuarial assumptions, steering parameters - this is the difference between "we have no idea what happened" and "here is the complete chain of custody."
|
||||
|
||||
This feature works for all temporal-aware load types (`UPDATE`, `TXTEMPORAL`, and `BITEMPORAL`) and respects SCD2 validity windows. And the whole process is built on the same open-source macro library that powers the rest of Data Controller - so you can inspect, test, and audit the code itself.
|
||||
|
||||
Full documentation is here: https://docs.datacontroller.io/restore/
|
||||
|
||||
<!--
|
||||
Source LinkedIn post:
|
||||
|
||||
|
||||
|
||||
"Can we put it back the way it was?" should never be a hard question to answer.
|
||||
|
||||
Yet for many teams managing reference data, mappings, and regulatory adjustments in SAS®, a wrong approval means exactly that — a forensic exercise, an uncomfortable audit conversation, and a quiet hope that nobody upstream consumed the bad data.
|
||||
|
||||
Data Controller now lets you ROLL BACK a table to any previous state.
|
||||
|
||||
Not by silently rewinding history - the one thing your auditor definitely does not want. Instead, the reversion is packaged as a brand NEW change that goes through the same review and approval as any other edit:
|
||||
|
||||
- The reversion diff is calculated and staged
|
||||
- An approver reviews it before anything is applied
|
||||
- The rollback itself lands in the audit trail - who, what, when, why
|
||||
|
||||
Nothing disappears. The original mistake, its correction, and every state in between all remain fully traceable.
|
||||
|
||||
Access rules apply exactly as they do for edits - the same group permissions, the same restrictions. A rollback cannot be used as a shortcut around your controls.
|
||||
|
||||
For regulated reporting data - the kind where "oops" is a reportable event, not a shrug - this closes the loop between catching an error and evidencing its correction.
|
||||
|
||||
The underlying process is open source.
|
||||
|
||||
Link in the comments below 👇
|
||||
|
||||
#sas #sasviya #datagovernance #datamanagement
|
||||
-->
|
||||
|
||||
<!-- Image prompt:
|
||||
A dramatic flat-design meme illustration in the Data Controller brand style: two high-speed trains colliding head-on at the center of the frame, with data rows and spreadsheet cells flying out of the impact. A group of onlookers in business-casual attire stand in the foreground, heads bowed, looking on in shared sadness. Dark navy background, teal/green and muted orange accents. The collision represents conflicting data changes; the sad onlookers are the data stewards. Minimal text, no labels. Slightly stylised, not gory - suitable for a B2B data product audience. 16:9 landscape, suitable as a blog/feed cover image.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 749 KiB |
|
After Width: | Height: | Size: 148 KiB |
@@ -0,0 +1,155 @@
|
||||
---
|
||||
title: '28 Ways to Be Missing in SAS'
|
||||
description: A SAS numeric missing is not a lone wolf - there are 28 of them, and Data Controller has supported all of them since v4. How they work, and what the validation rules do with them.
|
||||
date: '2026-09-22 09:00:00'
|
||||
author: 'Allan Bowe'
|
||||
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||
tags:
|
||||
- Special Missings
|
||||
- Data Quality
|
||||
previewImg: './cover.jpeg'
|
||||
---
|
||||
|
||||
# 28 Ways to Be Missing in SAS
|
||||
|
||||
A SAS numeric missing is not a lone wolf: the ordinary missing (`.`) is one of **28** distinct missing values a numeric variable can hold. The other 27 are written with a single character - the letters `A` to `Z`, or an underscore (`._`).
|
||||
|
||||
They exist because "missing" is usually not the whole story. A survey question that was never reached, a reading that was illegible, a value the respondent refused to give - in a well-run process those are different facts, and a lone `.` throws the difference away. Special missings record *why* the value is missing.
|
||||
|
||||
## They are numbers, not text
|
||||
|
||||
- Arithmetic on them yields missing - `.A + 1` is `.`
|
||||
- `PROC MEANS`, `PROC SUMMARY` and the other summarisation procedures exclude them, exactly as they exclude `.`
|
||||
- They sort below every non-missing number, in the order `._`, then `.`, then `.A` to `.Z`
|
||||
- `NMISS()` and `CMISS()` count them as missing
|
||||
- Converting one to text drops the period - `CATS(.A)` is the string `A`
|
||||
|
||||
One display detail is worth knowing: a regular missing prints as `.`, unless `OPTIONS MISSING=` changes that character - set it to blank and a regular missing renders as an empty cell. The option affects only the regular missing; `._` and `.A`-`.Z` always print as their own letter. So a blank cell in a SAS listing is still unambiguously a regular missing, and a lone letter is still a special one. Data Controller itself is unaffected either way - it sends a regular missing to the browser as `null` and a special missing as its letter.
|
||||
|
||||
In a SAS dataset they are written with a leading period (`.A`, `.B` ... `._`). In Data Controller you type the letter or the underscore, with or without that period - `.a` and `a` are the same missing - and the letter is not case sensitive. Two letters, or a letter mixed with a number, are refused rather than guessed.
|
||||
|
||||
There is one cell where the letter cannot be typed at all. A numeric column that carries a date, datetime or time format is edited through a date picker rather than the numeric editor, and a picker accepts only a date.
|
||||
|
||||
## Carrying them between the browser and SAS
|
||||
|
||||
The Data Controller frontend and the SAS backend exchange data as JSON, and JSON has no way to express a letter as a numeric value - `A` is a string. The conversion is handled in the open source [SASjs Adapter](https://github.com/sasjs/adapter#variable-types):
|
||||
|
||||
- The adapter infers each column's SAS type from the values it is given. All numeric values mean a numeric column, all strings mean a character column, and a column holding a single character (`a`-`z`, `_` or `.`) alongside numeric values is numeric, with the lone characters written to SAS as special missings.
|
||||
- `null` becomes `.` or an empty string, according to the type derived for that column.
|
||||
- Two cases cannot be inferred from the values alone: a numeric column containing *only* special missings looks like a single character column, and a character column containing only nulls looks numeric. For those, the adapter accepts an explicit format for the column - and Data Controller sends one automatically, because it already knows each column's SAS format from the metadata returned by the backend.
|
||||
- A value that is neither a number nor a single valid character is refused rather than guessed - `aaaa`, or `!` in a numeric column.
|
||||
- A lone `.` is accepted as another way of typing the regular missing.
|
||||
|
||||
There is nothing to configure. Special missings are available by default, for numeric cells - a date, datetime or time formatted column aside, since those edit through a date picker.
|
||||
|
||||
Once in SAS they are ordinary values, so they are what the approval DIFF screen compares, and the DIFF's formatted / unformatted switch shows either the formatted representation or the raw value - useful for confirming exactly which missing was set.
|
||||
|
||||
## What the Data Controller validation rules do with them
|
||||
|
||||
These are Data Controller's own rules, configured per column in the `MPE_VALIDATIONS` table and applied in the browser as you edit and submit.
|
||||
|
||||
- `NOTNULL` - rejects one. A special missing is a missing value, so it fails the rule, and a physical NOT NULL constraint on the target table rejects it as well. A primary key column is treated as NOT NULL whether or not a rule is configured for it
|
||||
- `HARDREGEX` - checked against the pattern like any other value; unlike blanks and the plain `.`, special missings are **not** exempt, so a numeric column that carries them needs a pattern which allows for a single letter
|
||||
- `SOFTREGEX` - the same check, but a failure is only a warning rather than a block, and it is ignored entirely if the column also has a `HARDREGEX` rule
|
||||
- `SOFTSELECT` / `HARDSELECT` - both support them. The dropdown lists a special missing as a bare letter alongside the ordinary values, and a hard rule then accepts it like any other listed value - it still rejects a value that is not in the list
|
||||
- `ROUND` - no effect. It only rounds values that are numbers, so a special missing is left as it was typed
|
||||
|
||||
The range rules compare in the order SAS itself uses, which is what lets a range be written in special missings. SAS puts every missing below every non-missing value, and orders the missing values among themselves: `._` is the lowest, then the regular missing, then `.A` through `.Z`. A range therefore means exactly what SAS would mean by it:
|
||||
|
||||
- `MINVAL .A` with `MAXVAL .C` accepts `.B` and rejects `.D`, and a blank - the regular missing - fails that floor because it sorts below `.A`
|
||||
- a number sits above every missing, so it passes a floor of `.A` and fails a ceiling of `.C`
|
||||
- against a numeric bound the ordering does the obvious thing: a missing sorts below every number, so it fails a `MINVAL` of 1 and passes a `MAXVAL` of 100. Use `NOTNULL` if the column has to be populated
|
||||
- a rule value that is neither a number nor a special missing - a typo such as `..` or `AB` - satisfies nothing, so the column fails until the rule is corrected
|
||||
|
||||
A formula is the one case where a special missing genuinely does not work:
|
||||
|
||||
- `HARDFORMULA` / `SOFTFORMULA` - a formula that reads a special-missing cell does not compute; the grid's spreadsheet engine returns `#VALUE!` for that row, where the plain `.` contributes 0
|
||||
|
||||
`CASE` (`UPCASE` / `LOWCASE`) is a character rule, so it does not apply: SAS hands the browser a special missing as an uppercase letter, so there is no case to enforce, and a `CASE` rule on a numeric column would reject the column's numbers rather than the missing.
|
||||
|
||||
Worth knowing about the regex rules: although the pattern is written in SAS PRX syntax, the check itself runs in the browser - SAS only parses the pattern (`PRXPARSE`) when the rule is saved - so the two engines can disagree on exotic patterns, and SAS pads a numeric-to-character conversion, so a pattern re-used in SAS needs `strip()` for an anchored match.
|
||||
|
||||
In short, a special missing counts as a value for the pattern and dropdown rules, and takes its own place in the order for the range rules: it sits below every number, so a numeric minimum rejects it and a numeric maximum accepts it, while a range written in special missings is decided among the missing values themselves.
|
||||
|
||||
## See it in action
|
||||
|
||||
The recording below runs the whole cycle on one table: entering special missings, the rules that reject them, submitting the changes, approving them, and reviewing the DIFF - including a change from one special missing to another, and the formatted / unformatted switch on a date column.
|
||||
|
||||
It also shows the range rules doing what the section above describes. A special missing is refused by a numeric `MINVAL` and accepted by a numeric `MAXVAL`, and on a column carrying `MINVAL .A` with `MAXVAL .C`, `.B` is taken while `.D` is refused.
|
||||
|
||||
<div style="position: relative; padding-top: 56.25%; margin-bottom: 2rem;"><iframe title="Special Missings in Data Controller" width="100%" height="100%" src="https://vid.4gl.io/videos/embed/9UQZzCNBU3zPNQYxdzyV3A?peertubeLink=0" style="border: 0px; position: absolute; inset: 0px;" allow="fullscreen" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe></div>
|
||||
|
||||
<!--
|
||||
LinkedIn version of this post - publish it with the "Special Missings in Data
|
||||
Controller" video attached. Kept in sync with the copy above: same points, same
|
||||
claims, same order.
|
||||
|
||||
A SAS numeric missing is not a lone wolf. There are 28 of them.
|
||||
|
||||
The ordinary missing (.) is the best known of the 28. The other 27 are single characters - the letters A to Z, or an underscore (._) - and they let you record WHY a value is missing: the question was never reached, the reading was illegible, the respondent refused.
|
||||
|
||||
They are real numeric values, not text:
|
||||
|
||||
- .A + 1 is .
|
||||
- PROC MEANS excludes them, like any other missing
|
||||
- they sort below every number: ._ then . then .A to .Z
|
||||
- NMISS() and CMISS() count them as missing
|
||||
- a regular missing prints as . unless OPTIONS MISSING= changes it - and that option never touches a special missing
|
||||
|
||||
The hard part is every tool that is not SAS. JSON has no way to say "this number is a letter" - A is just a string. So a value that is perfectly legal in a SAS dataset quietly breaks in the browser, in Excel, in an API.
|
||||
|
||||
We solved that in the open source SASjs Adapter, and it has been in Data Controller for SAS since v4:
|
||||
|
||||
- the adapter infers the column type from the values, so a column of numbers that also holds a lone letter is written to SAS as numeric, with the letter as a special missing
|
||||
- where the type cannot be inferred - a numeric column holding ONLY special missings - Data Controller passes the column format explicitly
|
||||
- you type the letter or the underscore, with or without the period (a, .a, _, ._), and case does not matter
|
||||
- one exception: a date, datetime or time formatted numeric column edits through a date picker, which takes only a date
|
||||
|
||||
How they behave in Data Controller's validation rules:
|
||||
|
||||
- NOTNULL rejects one: a special missing is a missing value, so it fails the rule, and a physical NOT NULL constraint on the target table rejects it too. A primary key column is NOT NULL whether or not a rule is configured
|
||||
- HARDREGEX checks it against the pattern, unlike blanks and plain .
|
||||
- SOFTREGEX warns instead of blocking (and is ignored if the column also has a HARDREGEX)
|
||||
- SOFTSELECT and HARDSELECT both support them - the dropdown lists the missing as a bare letter, and a hard rule accepts it like any other listed value
|
||||
- ROUND leaves it alone, because it only rounds numbers
|
||||
- a HARDFORMULA/SOFTFORMULA that reads a special-missing cell returns #VALUE! rather than a number
|
||||
|
||||
The range rules compare in SAS's own order, so a range can be written in special missings: MINVAL .A with MAXVAL .C accepts .B and rejects .D. A missing sorts below every number, so it fails a numeric MINVAL and passes a numeric MAXVAL; a number sits above every missing.
|
||||
|
||||
In short: a value for the pattern and dropdown rules, and its own place in the order for the range rules - below every number, so a numeric minimum rejects it and a numeric maximum accepts it.
|
||||
|
||||
The video shows the whole cycle - entering them, the rejections, submit, approve, and the DIFF, including a range written in special missings.
|
||||
|
||||
#sas #datacapture #mdm #dataquality
|
||||
|
||||
video: https://vid.4gl.io/w/9UQZzCNBU3zPNQYxdzyV3A
|
||||
|
||||
Image prompt: Cinematic editorial cover illustration: a large pack of wolves, a
|
||||
dozen or more, spread wide and moving together across a vast snow plain at dusk,
|
||||
seen from a low wide angle. One wolf stands apart from the group on the left of
|
||||
frame, turned back toward the pack. The pack is rendered in cool blue-grey and
|
||||
silver; the lone wolf catches the only warm light in the scene, a single low
|
||||
amber sun. Overcast dusk sky, faint falling snow, long soft shadows, generous
|
||||
empty sky and snow to the upper third so the wide crop breathes. Painterly
|
||||
digital illustration, muted desaturated palette, soft depth of field with the
|
||||
distant wolves falling out of focus, no text, no letters, no numbers, no logos,
|
||||
no watermark.
|
||||
|
||||
Constraints: no text, no letters, no numbers, no digits, no symbols, no
|
||||
captions, no logos, no watermark, no signature, no border, no frame, no collar,
|
||||
no harness, no humans, no buildings, and do not ask for an exact head count of
|
||||
28 - a crowded pack reads worse than a dozen clear animals, and the number
|
||||
belongs in the headline, not the artwork. The no-text rule matters more than
|
||||
usual here: the subject is letters standing in for numbers, so a stray glyph
|
||||
anywhere undercuts the cover.
|
||||
|
||||
Output: save as ./cover.jpeg at 1.91:1 (1200x627, matching the other feed
|
||||
covers and doubling as the LinkedIn share card), and set
|
||||
previewImg: './cover.jpeg' in the front matter above. Do not also embed the
|
||||
image in the markdown body. Full prompt, variants and rationale:
|
||||
https://paste.4gl.io/?513711f5bde3610e#9LZCA9xzQHDy3CycXmsWU5AoVNtjBZmPotUHHtHTAerF
|
||||
|
||||
Fallback variant if the pack composition comes back muddled: a single wolf
|
||||
standing alone on a snow plain at dusk, its shadow stretching toward a distant
|
||||
pack reduced to small silhouettes on the horizon.
|
||||
-->
|
||||
@@ -0,0 +1,158 @@
|
||||
---
|
||||
title: 'Support Diagnostics: browser_info and browser_url_vars'
|
||||
description: Data Controller sends two input tables with the startup service and with every service that runs customer code - the browser context and the page URL parameters. A support ticket can be answered from the job log, and a hook can read them.
|
||||
date: '2026-09-27 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './support-diagnostics.jpeg'
|
||||
---
|
||||
|
||||
# Support Diagnostics: browser_info and browser_url_vars
|
||||
|
||||
A support ticket usually starts with a round of questions. Which browser? Which timezone? Which build? What was in the URL? Data Controller now answers them itself. The frontend sends two input tables - `browser_info` and `browser_url_vars` - with the startup service and with every service that runs customer-provided code, so the context of a session is in the job log before anyone asks for it, and a hook script can read it.
|
||||
|
||||
This ships with the next Data Controller release.
|
||||
|
||||
## The two tables
|
||||
|
||||
Both are ordinary input tables on the request: the adapter turns each array in the payload into a table in the service's WORK library, so they arrive as `work.browser_info` and `work.browser_url_vars`.
|
||||
|
||||
### browser_info
|
||||
|
||||
A single row, describing the client that made the request:
|
||||
|
||||
| Column | What it holds |
|
||||
|---|---|
|
||||
| `url` | The URL of the Data Controller page itself (the iframe), not the document embedding it - so an embedded editor is distinguishable from a standalone one. |
|
||||
| `referrer` | The embedding document, from `document.referrer`. For an embedded report this is the report URL. |
|
||||
| `timezone` | The browser's IANA timezone, e.g. `Europe/Berlin`. |
|
||||
| `tz_offset` | The browser's UTC offset in minutes, as `Date.getTimezoneOffset()` returns it - positive west of UTC, so `Europe/Berlin` in summer is `-120`. |
|
||||
| `locale` | The browser locale, e.g. `en-GB`. |
|
||||
| `dc_version` | The Data Controller build, as `appinfo()` reports it. |
|
||||
| `adapter_version` | The `@sasjs/adapter` version the client was built with. |
|
||||
| `browser` | The browser family, parsed from the user agent. |
|
||||
| `browser_version` | The browser version. |
|
||||
| `platform` | The operating system. |
|
||||
| `user_agent` | The raw `navigator.userAgent` string. |
|
||||
|
||||
### browser_url_vars
|
||||
|
||||
One row per URL parameter:
|
||||
|
||||
| Column | What it holds |
|
||||
|---|---|
|
||||
| `name` | The parameter name. |
|
||||
| `value` | The parameter value. |
|
||||
|
||||
Parameters are read from both the search string and the hash query string, because Angular routes carry them after the `#`. Where the same name appears in both, the search string wins. The table is sent only when the page URL has at least one parameter.
|
||||
|
||||
## Which services receive them
|
||||
|
||||
| Service | When |
|
||||
|---|---|
|
||||
| `public/startupservice` | Once per session, so every job log carries the session context |
|
||||
| `editors/getdata` | The PRE_EDIT_HOOK |
|
||||
| `editors/stagedata` | The POST_EDIT_HOOK, through the loader |
|
||||
| `editors/restore` | The POST_EDIT_HOOK, through the loader |
|
||||
| `editors/getdynamiccolvals` | The dynamic cell dropdown programs |
|
||||
| `auditors/postdata` | The PRE_APPROVE_HOOK and POST_APPROVE_HOOK |
|
||||
| `editors/loadfile` | The POST_EDIT_HOOK - see the caveat below |
|
||||
|
||||
Those are the services that run customer code. The viewer's `viewlibs`, `viewtables` and `viewdata`, the metadata services and `usernav` receive neither table, so the high-frequency calls stay lean.
|
||||
|
||||
## Reading them in a hook
|
||||
|
||||
A hook script is `%include`d into the running service, so it shares the service's WORK library and can read the tables directly. Take an embedded editor opened at:
|
||||
|
||||
```
|
||||
https://datacontroller.io/#/editor/SASHELP.CLASS?labels=true&embed=va
|
||||
```
|
||||
|
||||
`browser_url_vars` then holds a row per parameter - `labels=true` and `embed=va` - and a hook can read either by name:
|
||||
|
||||
```sas
|
||||
%let labels=N;
|
||||
%let embed=N;
|
||||
|
||||
%if %sysfunc(exist(work.browser_url_vars)) %then %do;
|
||||
proc sql noprint;
|
||||
select value into :labels
|
||||
from work.browser_url_vars
|
||||
where name = 'labels';
|
||||
select value into :embed
|
||||
from work.browser_url_vars
|
||||
where name = 'embed';
|
||||
quit;
|
||||
%end;
|
||||
```
|
||||
|
||||
Or branch on what the user's browser reports:
|
||||
|
||||
```sas
|
||||
data _null_;
|
||||
set work.browser_info;
|
||||
call symputx('dc_timezone', timezone);
|
||||
call symputx('dc_locale', locale);
|
||||
run;
|
||||
```
|
||||
|
||||
Note the `exist()` guard. A service called directly, or by a frontend older than this feature, has no `work.browser_info` at all, so an unguarded `set` or `select` would fail the job. Guard first, then read.
|
||||
|
||||
## In the job log
|
||||
|
||||
Turn debug on and the session initialisation writes both tables to the log, so a ticket can be diagnosed without a second round of questions:
|
||||
|
||||
```
|
||||
NOTE: MPEINIT: work.browser_url_vars:
|
||||
name=embed value=va
|
||||
NOTE: MPEINIT: work.browser_info:
|
||||
url=... referrer=... timezone=Europe/Berlin tz_offset=-120 locale=en-GB
|
||||
dc_version=... adapter_version=... browser=Chrome browser_version=...
|
||||
platform=Linux user_agent=...
|
||||
```
|
||||
|
||||
The debug value depends on the path - `&_debug=131` on the Compute API and SAS 9, `&_debug=128` on the Viya web (JES) path with `runAsTask` enabled, which is what the frontend sends there. Either way, switching debug on in the app is enough.
|
||||
|
||||
## Worth knowing
|
||||
|
||||
- The values are supplied by the client. Treat them as diagnostics hints, never as a security boundary.
|
||||
- `editors/loadfile` is reached through the adapter's multipart file upload, which carries no input tables, so in normal use that service - and the hook it runs - does not receive them. It is on the list for completeness.
|
||||
- Nothing else pays for the tables: they travel only with the services above.
|
||||
|
||||
Full detail, including the column definitions and the log output, is in the [troubleshooting documentation](https://docs.datacontroller.io/dci-troubleshooting/).
|
||||
|
||||
<!--
|
||||
Source LinkedIn post:
|
||||
|
||||
A support ticket starts with questions. Which browser? Which timezone? Which build? What was in the URL?
|
||||
|
||||
Data Controller now answers them itself.
|
||||
|
||||
Two tables travel with the startup service, and with every service that runs your own code:
|
||||
|
||||
→ browser_info - one row: browser and version, platform, timezone and UTC offset, locale, the DC and adapter builds, the page URL and the referrer
|
||||
|
||||
→ browser_url_vars - one row per URL parameter, name and value, read straight into a macro variable
|
||||
|
||||
Both land in the job log when debug is on, so a ticket can be diagnosed from the log instead of a round trip. Both are readable from a hook script - %include shares the service's WORK library, so a hook can branch on the user's timezone, or pick up a parameter from the URL.
|
||||
|
||||
They are client supplied values: diagnostics hints, not a security boundary. Guard with %sysfunc(exist()) - a service called directly, or an older frontend, sends nothing.
|
||||
|
||||
✅ Full detail in the Data Controller documentation - link in the comments.
|
||||
|
||||
#sas #sasviya #datagovernance #datacapture #sasjs
|
||||
-->
|
||||
|
||||
<!-- Image prompt:
|
||||
Generate a 16:9 landscape illustration for a B2B data-governance blog cover, 1200x627.
|
||||
|
||||
Style: flat vector illustration, dark slate background (#314351) with a faint evenly spaced dot grid, brand green accent (#90c445), lighter slate panels (#2e4252), soft diffused shadows, subtle depth.
|
||||
|
||||
Scene: two rounded panels side by side, each with a header bar in brand green and four rows of rounded cells. The left panel carries short uniform value blocks (the browser context); the right panel carries name-and-value pairs, drawn as a narrow block beside a wider one (the URL parameters), with a single row in the left panel highlighted in green. Between and below the panels a thin vertical connector line runs down to a wide, flat log strip of alternating short green rules on a lighter slate panel, suggesting the tables being written to a job log.
|
||||
|
||||
Minimal text, no letters, no numbers, no logos, no watermark. Keep the subject inside the central square so a 1:1 crop is safe; the outer left and right thirds may be croppable background only.
|
||||
|
||||
Output: save as ./support-diagnostics.jpeg at 1200x627 (matching the other feed covers, and doubling as the LinkedIn share card). Do not also embed the image in the markdown body.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 53 KiB |
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: 'Data Controller for SAS® - Use Cases and Features'
|
||||
description: A rundown of Data Controller use cases and features - from secure data capture and approval workflows to SCD2 load routines, validation rules, and data lineage.
|
||||
date: '2026-08-12 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './usecases.jpeg'
|
||||
---
|
||||
|
||||
# Data Controller for SAS® - Use Cases and Features
|
||||
|
||||
What can you actually *do* with Data Controller? Here's the rundown:
|
||||
|
||||
- Data viewer with search, filter, and URL links - without table locks
|
||||
- Secure data (stop end-user write access to production tables)
|
||||
- Configure inputs in seconds (vs days or weeks for ETL jobs)
|
||||
- Preconfigured load routines including SCD2 and Bitemporal
|
||||
- Avoid exposing sensitive data in unsecured staging areas
|
||||
- Enable SAS workflows (trigger a SAS job on data change)
|
||||
- Prevent spreadsheet changes from halting batch flows
|
||||
- Email alerts when data is changed / about to change
|
||||
- Audit manual overrides on regulatory reporting data
|
||||
- Four-eyes (or more) approval for all data changes
|
||||
- View and amend formats in SAS format catalogs
|
||||
- Export DDL in SAS, TSQL or PGSQL format
|
||||
- Explore SAS 9 metadata and data lineage
|
||||
- Work with special SAS missing numerics
|
||||
- Upload password-protected Excel files
|
||||
- Upload directly from CSV or Excel
|
||||
- Composite / retained key support
|
||||
- Enforce data quality at source
|
||||
- Column and Row Level Security
|
||||
- Advanced validation rules
|
||||
|
||||
Our software is used and trusted (in production) by:
|
||||
|
||||
- Governments
|
||||
- Insurers
|
||||
- Pharma
|
||||
- Banks
|
||||
|
||||
Plus various other industry sectors - references available.
|
||||
|
||||
Would you like to kick the tyres? [Get in touch](/contact) for details.
|
||||
|
||||
<!--
|
||||
Source LinkedIn post:
|
||||
|
||||
Data Controller for SAS® Use Cases and Features:
|
||||
|
||||
✓ Data viewer with search, filter, and URL links - without table locks
|
||||
✓ Secure data (stop end-user write access to production tables)
|
||||
✓ Configure inputs in seconds (vs days or weeks for ETL jobs)
|
||||
✓ Preconfigured load routines including SCD2 and Bitemporal
|
||||
✓ Avoid exposing sensitive data in unsecured staging areas
|
||||
✓ Enable SAS workflows (trigger a SAS job on data change)
|
||||
✓ Prevent spreadsheet changes from halting batch flows
|
||||
✓ Email alerts when data is changed / about to change
|
||||
✓ Audit manual overrides on regulatory reporting data
|
||||
✓ Four-eyes (or more) approval for all data changes
|
||||
✓ View and amend formats in SAS format catalogs
|
||||
✓ Export DDL in SAS, TSQL or PGSQL format
|
||||
✓ Explore SAS 9 metadata and data lineage
|
||||
✓ Work with special SAS missing numerics
|
||||
✓ Upload password-protected Excel files
|
||||
✓ Upload directly from CSV or Excel
|
||||
✓ Composite / retained key support
|
||||
✓ Enforce data quality at source
|
||||
✓ Column and Row Level Security
|
||||
✓ Advanced validation rules
|
||||
|
||||
Our software is used and trusted (in production) by:
|
||||
|
||||
- Governments
|
||||
- Insurers
|
||||
- Pharma
|
||||
- Banks
|
||||
|
||||
Plus various other industry sectors - references available.
|
||||
|
||||
Would you like to kick the tires? DM for details.
|
||||
|
||||
#sas #sasviya #dataquality #datacapture
|
||||
|
||||
Image prompt:
|
||||
|
||||
Flat vector illustration, dark slate blue background (#314351), wide 1.91:1 banner. A clean modern data dashboard interface floating in the centre showing a data grid/table with a search bar and filter icons. Around it, a neat ring of small white and light-grey icons representing governance features: a padlock (security), a magnifying glass over rows (audit), two checkmarks side by side (4-eyes approval), an envelope (email alerts), an Excel/spreadsheet icon with an upward arrow (upload), a branching flow diagram (workflow/lineage), and a shield with a tick (data quality). Thin connecting lines link the icons to the central dashboard, suggesting control and flow. Minimal, professional, corporate SaaS style; subtle depth with soft shadows; accent colour a muted red-orange (#d3531a) used sparingly on one or two icons. No text, no words, no letters. High contrast, crisp edges, suitable as a LinkedIn post image.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 353 KiB |
@@ -3,7 +3,7 @@ title: 'v7.12.0: Regex Validation Rules'
|
||||
description: Define regex patterns once and have every value checked automatically - with hard rules that block bad data and soft rules to flag it for a second look.
|
||||
date: '2026-07-28 15:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data_controller
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
---
|
||||
|
||||
@@ -3,7 +3,7 @@ title: "Welcome to the Data Controller Feed"
|
||||
description: A new home for quick announcements, social media updates and other bite-sized news from the Data Controller team.
|
||||
date: '2026-07-17 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data_controller
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
---
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: 'Data Controller for SAS® - Who Is It For?'
|
||||
description: Five roles, five questions - what Data Controller for SAS® answers for administrators, auditors, managers, data owners and purchasers.
|
||||
date: '2026-09-26 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './who-is-data-controller-for.jpeg'
|
||||
---
|
||||
|
||||
# Data Controller for SAS® - Who Is It For?
|
||||
|
||||
Data Controller for SAS® gives business users a safe way to add, modify and delete data in SAS tables and databases, without handing them write access to production. That one capability answers a different question for each role that touches the data - so here is the short version, role by role.
|
||||
|
||||
## Data Administrators
|
||||
|
||||
Would you grant self-serve data updates in SAS if there was no risk to the data model? That is the trade Data Controller offers. End users never get direct write access to the target tables - every change is staged, validated at the point of entry, and applied by the service itself. Data Controller changes data, not structure, so your data model stays as it is.
|
||||
|
||||
## Data Auditors
|
||||
|
||||
Do you need to know who made a particular data change, what that change was, when it was made, and who signed it off? Every submission carries a full audit trail, and any associated Excel file is retained alongside it, so the source of a change can be reviewed long after it was loaded.
|
||||
|
||||
## Data Managers
|
||||
|
||||
Do you want to load, modify or delete production data without writing repetitive code, saving sensitive data to shared filesystems, building and deploying jobs, or crafting long-winded change requests? Adding a new editable table is configuration rather than development - the grid is generated from the target table's metadata, and the load type (composite or retained keys, SCD2, bitemporal) is picked from a list.
|
||||
|
||||
## Data Owners
|
||||
|
||||
Do you like to review data changes before they are applied? Approvers see the diff - new, changed and deleted rows - and nothing lands until it is signed off. Email alerts can tell you when data is changed or about to change, and a completed load can trigger a SAS job downstream. If the wrong change gets approved anyway, the table can be rolled back to a previous state - and the rollback is itself a new, reviewable change, so the audit trail keeps both the mistake and the correction.
|
||||
|
||||
## Purchasers
|
||||
|
||||
How about a business friendly, no lock-in software licence? The price list is public, every package includes unlimited users, and a perpetual licence is available. The Community package is free, so evaluating Data Controller costs nothing but the installation.
|
||||
|
||||
## What it is, in one line
|
||||
|
||||
Data Controller for SAS® is a robust solution for secure data uploads into any SAS-connected database, with full audit trail and approval mechanism. Configuration is quick, installation is easy, and everything runs on your own SAS platform with no external calls.
|
||||
|
||||
Multiple load types are supported (composite and retained keys, SCD2, bitemporal, format catalogs), data can be validated at the point of entry, and Excel end-user computing files can be dragged straight into SAS. Data Rollback, Data Catalog and Data Viewer come as standard.
|
||||
|
||||
Data Control.
|
||||
|
||||
Want to see it working on your own data? [Book a Discovery Call](/contact).
|
||||
|
||||
<!--
|
||||
Source LinkedIn post:
|
||||
|
||||
Data Administrators → would you grant self-serve data updates in SAS, if there was no risk to the data model?
|
||||
|
||||
Data Auditors → do you require oversight of who made a particular data change, what that change was, when it was made, and who signed it off? And to review the original excel, if used as a source?
|
||||
|
||||
Data Managers → do you prefer to load / modify / delete production data WITHOUT writing repetitive code, saving sensitive data to shared filesystems, building & deploying jobs, or crafting long-winded change requests?
|
||||
|
||||
Data Owners → do you like to review data changes before they are applied? How about receiving email alerts or the ability to set up job triggers after tables are updated?
|
||||
|
||||
Purchasers → how about a business friendly, no lock-in software licence?
|
||||
|
||||
Data Controller for SAS® is a robust solution for secure data uploads into any SAS-connected database, with full audit trail and approval mechanism.
|
||||
|
||||
Installation is easy and configuration is quick ⚡ Multiple load types supported (composite / retained keys, SCD2, Bitemporal, Format catalogs). Perform data validation at point of entry. Drag & drop Excel EUCs straight into SAS.
|
||||
|
||||
Data Rollback, Data Catalog, Data Viewer.
|
||||
|
||||
Data Control. ✅
|
||||
|
||||
#sas #sasviya #sasusers #datagovernance
|
||||
|
||||
https://lnkd.in/dunqWhnt
|
||||
|
||||
Image prompt:
|
||||
|
||||
Flat vector illustration, dark slate blue background (#314351), wide 1.91:1 banner (1200 x 627). Five simplified human figures stand in a shallow arc across the centre, each rendered in flat corporate style with minimal facial detail: an administrator beside a padlock, an auditor with a magnifying glass over a document, a manager holding a spreadsheet with an upward arrow, a data owner holding an approval tick, and a purchaser holding a price tag. Behind them, a translucent dashboard panel shows a data grid with one row highlighted as a pending change, and a small diff of new, changed and deleted rows. Thin connecting lines link each figure to the panel, suggesting that every role sees the same controlled workflow. Bright green accent (#90c445) used sparingly on the approval tick, the highlighted row and the connecting lines. Subtle depth with soft shadows, crisp edges. No text, no words, no letters, no logos, no watermarks. Keep the figures and the panel inside the central square so the image is safe to crop to 1:1 - the outer left and right thirds are background only.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 138 KiB |
@@ -3,7 +3,7 @@ title: 'Zen and the Art of Data Quality'
|
||||
description: Pirsig's motorcycle classic meets the National Bank of Belgium's DQ guidance - and the three layers of data quality defence Data Controller for SAS provides.
|
||||
date: '2026-08-07 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data_controller
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
previewImg: './dilbert.jpeg'
|
||||
tags:
|
||||
- Announcements
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: 'Zero-Code Business Data Capture for SAS®'
|
||||
description: Loading business data to IT-controlled databases should be simple, secure, and FAST. Data Controller for SAS® eliminates hand-cranked ETL flows.
|
||||
date: '2026-08-10 18:00:00'
|
||||
author: 'Allan Bowe'
|
||||
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './zero-code-business-data-capture-for-sas.jpeg'
|
||||
---
|
||||
|
||||
As a member of a Data & Analytics team in a regulated sector such as Banking or Insurance, you need to deliver outputs in a consistent and timely fashion to meet statutory and management reporting demands.
|
||||
|
||||
In order to do that, your team builds and maintains automated processes that capture, cleanse and integrate with centrally managed Data Warehouses.
|
||||
|
||||
The problem when capturing data created by business users (as opposed to IT-secured sources) is the cost to build, deploy and incident-manage ETL flows with constantly changing file formats and arrival frequencies.
|
||||
|
||||
This can result in 'blame-games' and delays in data delivery.
|
||||
|
||||
We strongly believe that loading business data to IT-controlled databases should be simple, secure, and fast.
|
||||
|
||||
That's why we created Data Controller for SAS® - a unique zero-code solution for capturing business data at source and moving it safely and securely into your IT-managed data store.
|
||||
|
||||
The result is the complete elimination of 'hand-cranked' ETL flows and other expensive home-grown solutions that eat up your OpEx budget and reduce your capacity to scale.
|
||||
|
||||
To see how easily Data Controller can be deployed in your organisation, book a [Discovery Call](https://datacontroller.io/contact/).
|
||||
|
After Width: | Height: | Size: 316 KiB |
@@ -8,7 +8,7 @@ module.exports = {
|
||||
summary: ``
|
||||
},
|
||||
social: {
|
||||
linkedin: `https://www.linkedin.com/showcase/data_controller/`
|
||||
linkedin: `https://www.linkedin.com/showcase/data-controller-for-sas/`
|
||||
}
|
||||
},
|
||||
pathPrefix: '',
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
"@types/react": "^18.3.31",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"bootstrap": "^5.0.0-beta3",
|
||||
"cloudron-surfer": "^6.4.1",
|
||||
"jquery": "^3.6.0",
|
||||
"typescript": "^7.0.2"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,940 @@
|
||||
{
|
||||
"$comment": "Advisories at critical/high accepted for this tree. scripts/audit-gate.mjs fails the build on anything not listed here, so removing an entry re-arms the gate for that advisory. Regenerate with: node scripts/audit-gate.mjs --write-baseline",
|
||||
"owner": "Site maintainers (allan@4gl.io)",
|
||||
"reason": "Gatsby 5 build toolchain: no non-breaking fix available upstream, and these are build-time dependencies of a statically generated site. Re-check on each Gatsby major bump.",
|
||||
"accepted": [
|
||||
{
|
||||
"pkg": "@ardatan/relay-compiler",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@babel/plugin-transform-modules-systemjs",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-fv7c-fp4j-7gwp",
|
||||
"title": "@babel/plugin-transform-modules-systemjs generates arbitrary code when compiling malicious input",
|
||||
"range": ">=7.12.0 <=7.29.3"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-codegen/add",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-codegen/core",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-codegen/plugin-helpers",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-codegen/schema-ast",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-codegen/typescript-operations",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-codegen/typescript",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-codegen/visitor-plugin-common",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-tools/code-file-loader",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-tools/graphql-tag-pluck",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-tools/load",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-tools/merge",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-tools/relay-operation-optimizer",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-tools/schema",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-tools/utils",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-7mx3-vvmw-hjmv",
|
||||
"title": "GraphQL Tools has prototype pollution in well-established utility function `mergeDeep`",
|
||||
"range": "<=12.0.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-35jp-ww65-95wh",
|
||||
"title": "axios Vulnerable to Full Man-in-the-Middle via Prototype Pollution Gadget in `config.proxy`",
|
||||
"range": ">=1.0.0 <1.16.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-3g43-6gmg-66jw",
|
||||
"title": "axios Vulnerable to Credential Theft and Response Hijacking via Prototype Pollution Gadget in Config Merge",
|
||||
"range": ">=1.0.0 <1.15.2"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-3p68-rc4w-qgx5",
|
||||
"title": "Axios has a NO_PROXY Hostname Normalization Bypass that Leads to SSRF",
|
||||
"range": ">=1.0.0 <1.15.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-3pq3-5fj3-cg6v",
|
||||
"title": "Axios: HTTP/2 adapter bypasses configured DNS lookup and proxy controls",
|
||||
"range": ">=1.13.0 <1.20.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-3w6x-2g7m-8v23",
|
||||
"title": "Axios: Invisible JSON Response Tampering via Prototype Pollution Gadget in `parseReviver`",
|
||||
"range": ">=1.0.0 <1.15.2"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-42h9-826w-cgv3",
|
||||
"title": "Axios: Excessive recursion in formDataToJSON can cause denial of service",
|
||||
"range": ">=1.0.0 <1.18.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-43fc-jf86-j433",
|
||||
"title": "Axios is Vulnerable to Denial of Service via __proto__ Key in mergeConfig",
|
||||
"range": ">=1.0.0 <=1.13.4"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-445q-vr5w-6q77",
|
||||
"title": "Axios: CRLF Injection in multipart/form-data body via unsanitized blob.type in formDataToStream",
|
||||
"range": ">=1.0.0 <1.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-4hqw-qxg8-jxx2",
|
||||
"title": "Axios: Fetch Adapter Header Injection via Inherited FormData getHeaders",
|
||||
"range": ">=1.12.0 <1.20.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-542g-h47m-68v8",
|
||||
"title": "Axios: Denial of Service via Unhandled 'error' Event in HTTP/2 ClientHttp2Session Initialization",
|
||||
"range": ">=1.13.0 <1.20.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-5c9x-8gcm-mpgx",
|
||||
"title": "Axios' HTTP adapter-streamed uploads bypass maxBodyLength when maxRedirects: 0",
|
||||
"range": ">=1.0.0 <1.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-62hf-57xw-28j9",
|
||||
"title": "Axios: unbounded recursion in toFormData causes DoS via deeply nested request data",
|
||||
"range": ">=1.0.0 <1.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-6chq-wfr3-2hj9",
|
||||
"title": "Axios: Header Injection via Prototype Pollution",
|
||||
"range": ">=1.0.0 <1.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-777c-7fjr-54vf",
|
||||
"title": "Allocation of Resources Without Limits or Throttling in Axios",
|
||||
"range": ">=1.7.0 <1.16.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-7q8q-rj6j-mhjq",
|
||||
"title": "Axios: Nested axios option objects can consume polluted prototype values",
|
||||
"range": ">=1.0.0 <1.18.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-898c-q2cr-xwhg",
|
||||
"title": "axios has DoS & Header Injection via Prototype Pollution Read-Side Gadgets in axios merge functions",
|
||||
"range": ">=1.0.0 <1.16.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-9fr6-4gfg-395g",
|
||||
"title": "Axios: Prototype-Pollution Gadget in the Default Instance Allows Inherited Object.prototype.method to Override HTTP Method",
|
||||
"range": ">=1.0.0 <1.20.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-fvcv-3m26-pcqx",
|
||||
"title": "Axios has Unrestricted Cloud Metadata Exfiltration via Header Injection Chain",
|
||||
"range": ">=1.0.0 <1.15.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-hfxv-24rg-xrqf",
|
||||
"title": "Axios: Regular Expression Denial of Service (ReDoS) via Cookie Name Injection",
|
||||
"range": ">=1.0.0 <1.16.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-j5f8-grm9-p9fc",
|
||||
"title": "Axios: Proxy-Authorization header leaks to redirect target when proxy is re-evaluated to direct connection",
|
||||
"range": ">=1.0.0 <1.16.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-j8rh-479h-cp32",
|
||||
"title": "Axios: Header Injection via Inherited headers After Minimal Interceptor",
|
||||
"range": ">=1.0.0 <1.20.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-jqh4-m9w3-8hp9",
|
||||
"title": "Axios: Fetch adapter `ReadableStream` uploads bypass `maxBodyLength`",
|
||||
"range": ">=1.7.0 <1.18.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-m7pr-hjqh-92cm",
|
||||
"title": "Axios: no_proxy bypass via IP alias allows SSRF",
|
||||
"range": ">=1.0.0 <1.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-mmx7-hfxf-jppx",
|
||||
"title": "Axios: Prototype pollution gadgets can alter axios request construction",
|
||||
"range": ">=1.0.0 <1.18.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-mwf2-3pr3-8698",
|
||||
"title": "Axios: HTTP/2 streamed uploads bypass `maxBodyLength`",
|
||||
"range": ">=1.13.0 <1.18.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-p92q-9vqr-4j8v",
|
||||
"title": "Axios: Proxy-Authorization Credential Leak to Origin Server Across HTTP-to-HTTPS Redirect in Axios Node.js HTTP Adapter",
|
||||
"range": ">=1.0.0 <1.16.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-pf86-5x62-jrwf",
|
||||
"title": "Axios: Prototype Pollution Gadgets - Response Tampering, Data Exfiltration, and Request Hijacking",
|
||||
"range": ">=1.0.0 <1.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-pmv8-rq9r-6j72",
|
||||
"title": "Axios: Deep formToJSON Key Recursion Can Cause Denial of Service",
|
||||
"range": ">=1.0.0 <1.18.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-pmwg-cvhr-8vh7",
|
||||
"title": "Axios: Incomplete Fix for CVE-2025-62718 — NO_PROXY Protection Bypassed via RFC 1122 Loopback Subnet (127.0.0.0/8) in Axios 1.15.0",
|
||||
"range": ">=1.0.0 <1.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-q8qp-cvcw-x6jj",
|
||||
"title": "Axios has prototype pollution read-side gadgets in HTTP adapter that allow credential injection and request hijacking",
|
||||
"range": ">=1.0.0 <1.15.2"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-vf2m-468p-8v99",
|
||||
"title": "Axios: HTTP adapter streamed responses bypass maxContentLength",
|
||||
"range": ">=1.0.0 <1.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-vh66-26gq-q6x8",
|
||||
"title": "Axios: Prototype pollution gadget in fetch adapter can alter outbound requests",
|
||||
"range": ">=1.7.0 <1.20.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-w9j2-pvgh-6h63",
|
||||
"title": "Axios: Authentication Bypass via Prototype Pollution Gadget in `validateStatus` Merge Strategy",
|
||||
"range": ">=1.0.0 <1.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-xhjh-pmcv-23jw",
|
||||
"title": "Axios: Null Byte Injection via Reverse-Encoding in AxiosURLSearchParams",
|
||||
"range": ">=1.0.0 <1.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-xx6v-rp6x-q39c",
|
||||
"title": "Axios: XSRF Token Cross-Origin Leakage via Prototype Pollution Gadget in `withXSRFToken` Boolean Coercion",
|
||||
"range": ">=1.0.0 <1.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "body-parser",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-27r3-jxmh-p828",
|
||||
"title": "body-parser vulnerable to Denial of Service via trailing bytes after a compressed request body",
|
||||
"range": "<1.20.9"
|
||||
},
|
||||
{
|
||||
"pkg": "body-parser",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-qwcr-r2fm-qrc7",
|
||||
"title": "body-parser vulnerable to denial of service when url encoding is enabled",
|
||||
"range": "<1.20.3"
|
||||
},
|
||||
{
|
||||
"pkg": "body-parser",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-v422-hmwv-36x6",
|
||||
"title": "body-parser vulnerable to denial of service when invalid limit value silently disables size enforcement",
|
||||
"range": "<1.20.6"
|
||||
},
|
||||
{
|
||||
"pkg": "brace-expansion",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-3jxr-9vmj-r5cp",
|
||||
"title": "brace-expansion: DoS via exponential-time expansion of consecutive non-expanding {} groups",
|
||||
"range": "<1.1.16"
|
||||
},
|
||||
{
|
||||
"pkg": "brace-expansion",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-6j4f-fj2g-mc7p",
|
||||
"title": "brace-expansion: DoS via uncontrolled recursion in parseCommaParts causing stack exhaustion",
|
||||
"range": "<1.1.19"
|
||||
},
|
||||
{
|
||||
"pkg": "brace-expansion",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-f886-m6hf-6m8v",
|
||||
"title": "brace-expansion: Zero-step sequence causes process hang and memory exhaustion",
|
||||
"range": "<1.1.13"
|
||||
},
|
||||
{
|
||||
"pkg": "brace-expansion",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-mh99-v99m-4gvg",
|
||||
"title": "brace-expansion: DoS via unbounded expansion length causing an out-of-memory process crash",
|
||||
"range": "<1.1.17"
|
||||
},
|
||||
{
|
||||
"pkg": "brace-expansion",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-q2hr-2g5m-vwhr",
|
||||
"title": "brace-expansion: Quadratic-time expansion of the `{a},b}` rewrite causes CPU denial of service",
|
||||
"range": "<1.1.21"
|
||||
},
|
||||
{
|
||||
"pkg": "brace-expansion",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-qhr7-859c-m2p7",
|
||||
"title": "brace-expansion: DoS via uncontrolled recursion on nested brace groups causing stack exhaustion",
|
||||
"range": "<1.1.20"
|
||||
},
|
||||
{
|
||||
"pkg": "brace-expansion",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-rgw5-rvv9-x895",
|
||||
"title": "brace-expansion: DoS via unbounded intermediate arrays, bypassing the CVE-2026-14257 mitigation",
|
||||
"range": "<1.1.18"
|
||||
},
|
||||
{
|
||||
"pkg": "brace-expansion",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-v6h2-p8h4-qcjw",
|
||||
"title": "brace-expansion Regular Expression Denial of Service vulnerability",
|
||||
"range": ">=1.0.0 <=1.1.11"
|
||||
},
|
||||
{
|
||||
"pkg": "browserslist",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-73wf-gq98-2v4g",
|
||||
"title": "Browserslist: Uncaught crash / prototype write via untrusted browserslist-stats.json custom stats (normalizeStats)",
|
||||
"range": "<=4.28.6"
|
||||
},
|
||||
{
|
||||
"pkg": "browserslist",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-c83g-rgw3-j3cx",
|
||||
"title": "Browserslist: Unbounded memory growth (no cache eviction) via distinct query results, leading to eventual OOM",
|
||||
"range": "<=4.28.6"
|
||||
},
|
||||
{
|
||||
"pkg": "compression",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-vc2v-76pw-4v95",
|
||||
"title": "compression vulnerable to Denial of Service via memory leak on premature response close",
|
||||
"range": "<1.8.2"
|
||||
},
|
||||
{
|
||||
"pkg": "cross-spawn",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-3xgq-45jj-v275",
|
||||
"title": "Regular Expression Denial of Service (ReDoS) in cross-spawn",
|
||||
"range": ">=7.0.0 <7.0.5"
|
||||
},
|
||||
{
|
||||
"pkg": "engine.io",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-2gc4-cqfq-p2gv",
|
||||
"title": "Socket.IO: Engine.IO Protocol Revision Mismatch DoS",
|
||||
"range": ">=6.6.0 <6.6.10"
|
||||
},
|
||||
{
|
||||
"pkg": "engine.io",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-gr94-w7qr-f4j3",
|
||||
"title": "Socket.IO: Engine.IO WebTransport SID DoS",
|
||||
"range": ">=6.5.0 <6.6.7"
|
||||
},
|
||||
{
|
||||
"pkg": "engine.io",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-r635-g3xr-vw7x",
|
||||
"title": "Socket.IO: Engine.IO Polling Transport Connection Exhaustion",
|
||||
"range": ">=4.1.0 <6.6.7"
|
||||
},
|
||||
{
|
||||
"pkg": "express",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-qw6h-vgh9-j6wx",
|
||||
"title": "express vulnerable to XSS via response.redirect()",
|
||||
"range": "<4.20.0"
|
||||
},
|
||||
{
|
||||
"pkg": "flatted",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-25h7-pfq9-p65f",
|
||||
"title": "flatted vulnerable to unbounded recursion DoS in parse() revive phase",
|
||||
"range": "<3.4.0"
|
||||
},
|
||||
{
|
||||
"pkg": "flatted",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-rf6f-7fwh-wjgh",
|
||||
"title": "Prototype Pollution via parse() in NodeJS flatted",
|
||||
"range": "<=3.4.1"
|
||||
},
|
||||
{
|
||||
"pkg": "form-data",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-hmw2-7cc7-3qxx",
|
||||
"title": "form-data: CRLF injection in form-data via unescaped multipart field names and filenames",
|
||||
"range": ">=4.0.0 <4.0.6"
|
||||
},
|
||||
{
|
||||
"pkg": "gatsby-plugin-manifest",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "gatsby-plugin-sharp",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "gatsby-plugin-utils",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "gatsby-sharp",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "gatsby-transformer-sharp",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "gatsby",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "http-cache-semantics",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-ch52-4w7c-c8xp",
|
||||
"title": "http-cache-semantics max-stale handling can disclose cross-user cached responses",
|
||||
"range": "<=4.2.0"
|
||||
},
|
||||
{
|
||||
"pkg": "immutable",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-v56q-mh7h-f735",
|
||||
"title": "Immutable.js `List` 32-bit trie overflow → unrecoverable DoS",
|
||||
"range": "<3.8.4"
|
||||
},
|
||||
{
|
||||
"pkg": "immutable",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-wf6x-7x77-mvgw",
|
||||
"title": "Immutable is vulnerable to Prototype Pollution",
|
||||
"range": "<3.8.3"
|
||||
},
|
||||
{
|
||||
"pkg": "immutable",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-xvcm-6775-5m9r",
|
||||
"title": "Immutable: Hash-collision algorithmic complexity denial of service in Immutable.Map/Set",
|
||||
"range": "<3.8.4"
|
||||
},
|
||||
{
|
||||
"pkg": "joi",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-6h2x-m376-mqjq",
|
||||
"title": "joi: Quadratic regular-expression backtracking in `Joi.string().isoDate()`",
|
||||
"range": ">=17.2.0 <17.13.7"
|
||||
},
|
||||
{
|
||||
"pkg": "joi",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-6w3j-5fw6-r9vr",
|
||||
"title": "joi: Prototype pollution via a `__proto__` language key in custom messages",
|
||||
"range": ">=17.2.0 <17.13.6"
|
||||
},
|
||||
{
|
||||
"pkg": "joi",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-gg4h-3hg2-grpc",
|
||||
"title": "joi: object().rename() with a template target can set the validated object's prototype",
|
||||
"range": ">=16.0.0 <17.13.5"
|
||||
},
|
||||
{
|
||||
"pkg": "joi",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-wr44-6hxh-3jwq",
|
||||
"title": "joi messages compilation allows prototype replacement through __proto__ error codes",
|
||||
"range": "<17.13.8"
|
||||
},
|
||||
{
|
||||
"pkg": "js-yaml",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-2883-xcg3-v3hh",
|
||||
"title": "js-yaml: maxTotalMergeKeys does not limit CPU use for empty merge sources",
|
||||
"range": ">=3.0.0 <3.15.2"
|
||||
},
|
||||
{
|
||||
"pkg": "js-yaml",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-52cp-r559-cp3m",
|
||||
"title": "js-yaml: YAML merge-key chains can force quadratic CPU consumption",
|
||||
"range": ">=3.0.0 <3.15.0"
|
||||
},
|
||||
{
|
||||
"pkg": "js-yaml",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-5p4m-2wfm-xmqj",
|
||||
"title": "JS-YAML: Quadratic CPU consumption in !!omap resolution (3.x and 4.x) — CVE-2026-59870 fix not backported",
|
||||
"range": ">=3.0.0 <3.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "js-yaml",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-h67p-54hq-rp68",
|
||||
"title": "JS-YAML: Quadratic-complexity DoS in merge key handling via repeated aliases",
|
||||
"range": "<3.15.0"
|
||||
},
|
||||
{
|
||||
"pkg": "js-yaml",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-mh29-5h37-fv8m",
|
||||
"title": "js-yaml has prototype pollution in merge (<<)",
|
||||
"range": "<3.14.2"
|
||||
},
|
||||
{
|
||||
"pkg": "lodash",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-f23m-r3pf-42rh",
|
||||
"title": "lodash vulnerable to Prototype Pollution via array path bypass in `_.unset` and `_.omit`",
|
||||
"range": "<=4.17.23"
|
||||
},
|
||||
{
|
||||
"pkg": "lodash",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-r5fr-rjxr-66jc",
|
||||
"title": "lodash vulnerable to Code Injection via `_.template` imports key names",
|
||||
"range": ">=4.0.0 <=4.17.23"
|
||||
},
|
||||
{
|
||||
"pkg": "lodash",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-xxjr-mmjv-4gpg",
|
||||
"title": "Lodash has Prototype Pollution Vulnerability in `_.unset` and `_.omit` functions",
|
||||
"range": ">=4.0.0 <=4.17.22"
|
||||
},
|
||||
{
|
||||
"pkg": "minimatch",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-23c5-xmqv-rm74",
|
||||
"title": "minimatch ReDoS: nested *() extglobs generate catastrophically backtracking regular expressions",
|
||||
"range": "<3.1.4"
|
||||
},
|
||||
{
|
||||
"pkg": "minimatch",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-3ppc-4f35-3m26",
|
||||
"title": "minimatch has a ReDoS via repeated wildcards with non-matching literal in pattern",
|
||||
"range": "<3.1.3"
|
||||
},
|
||||
{
|
||||
"pkg": "minimatch",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-7r86-cg39-jmmj",
|
||||
"title": "minimatch has ReDoS: matchOne() combinatorial backtracking via multiple non-adjacent GLOBSTAR segments",
|
||||
"range": "<3.1.3"
|
||||
},
|
||||
{
|
||||
"pkg": "multer",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-3p4h-7m6x-2hcm",
|
||||
"title": "Multer vulnerable to Denial of Service via incomplete cleanup of aborted uploads",
|
||||
"range": ">=2.0.0-alpha.1 <2.2.0"
|
||||
},
|
||||
{
|
||||
"pkg": "multer",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-535w-7cp7-47q4",
|
||||
"title": "multer vulnerable to Denial of Service via oversized array index in field names",
|
||||
"range": "<2.3.0"
|
||||
},
|
||||
{
|
||||
"pkg": "multer",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-5528-5vmv-3xc2",
|
||||
"title": "Multer Vulnerable to Denial of Service via Uncontrolled Recursion",
|
||||
"range": "<2.1.1"
|
||||
},
|
||||
{
|
||||
"pkg": "multer",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-72gw-mp4g-v24j",
|
||||
"title": "Multer vulnerable to Denial of Service via deeply nested field names",
|
||||
"range": ">=1.0.0 <2.2.0"
|
||||
},
|
||||
{
|
||||
"pkg": "multer",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-q7qp-rjg7-q26m",
|
||||
"title": "multer vulnerable to Denial of Service via a pending asynchronous fileFilter after request teardown",
|
||||
"range": ">=2.0.0 <2.5.0"
|
||||
},
|
||||
{
|
||||
"pkg": "multer",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-qvfw-j98x-7q72",
|
||||
"title": "multer vulnerable to file size limit bypass via async fileFilter race condition",
|
||||
"range": "<2.3.0"
|
||||
},
|
||||
{
|
||||
"pkg": "multer",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-v52c-386h-88mc",
|
||||
"title": "Multer vulnerable to Denial of Service via resource exhaustion",
|
||||
"range": "<2.1.0"
|
||||
},
|
||||
{
|
||||
"pkg": "multer",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-wc9g-mqfw-jrwm",
|
||||
"title": "multer vulnerable to Denial of Service via crafted multipart field names",
|
||||
"range": "<2.3.0"
|
||||
},
|
||||
{
|
||||
"pkg": "multer",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-xf7r-hgr6-v32p",
|
||||
"title": "Multer vulnerable to Denial of Service via incomplete cleanup",
|
||||
"range": "<2.1.0"
|
||||
},
|
||||
{
|
||||
"pkg": "nanoid",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-28wg-ghj8-5hjv",
|
||||
"title": "nanoid: non-secure generators can loop indefinitely with negative size",
|
||||
"range": "<3.3.16"
|
||||
},
|
||||
{
|
||||
"pkg": "nanoid",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-2v37-7h3g-55p8",
|
||||
"title": "nanoid: custom generators can loop indefinitely when size is zero",
|
||||
"range": "<3.3.18"
|
||||
},
|
||||
{
|
||||
"pkg": "nanoid",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-mwcw-c2x4-8c55",
|
||||
"title": "Predictable results in nanoid generation when given non-integer values",
|
||||
"range": "<3.3.8"
|
||||
},
|
||||
{
|
||||
"pkg": "nanoid",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-xwg4-73v4-xw9w",
|
||||
"title": "nanoid: Integer Overflow or Wraparound",
|
||||
"range": "<3.3.12"
|
||||
},
|
||||
{
|
||||
"pkg": "path-to-regexp",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-37ch-88jc-xwx2",
|
||||
"title": "path-to-regexp vulnerable to Regular Expression Denial of Service via multiple route parameters",
|
||||
"range": "<0.1.13"
|
||||
},
|
||||
{
|
||||
"pkg": "path-to-regexp",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-9wv6-86v2-598j",
|
||||
"title": "path-to-regexp outputs backtracking regular expressions",
|
||||
"range": "<0.1.10"
|
||||
},
|
||||
{
|
||||
"pkg": "path-to-regexp",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-rhx6-c78j-4q9w",
|
||||
"title": "path-to-regexp contains a ReDoS",
|
||||
"range": "<0.1.12"
|
||||
},
|
||||
{
|
||||
"pkg": "picomatch",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-3v7f-55p6-f55p",
|
||||
"title": "Picomatch: Method Injection in POSIX Character Classes causes incorrect Glob Matching",
|
||||
"range": "<2.3.2"
|
||||
},
|
||||
{
|
||||
"pkg": "picomatch",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-c2c7-rcm5-vvqj",
|
||||
"title": "Picomatch has a ReDoS vulnerability via extglob quantifiers",
|
||||
"range": "<2.3.2"
|
||||
},
|
||||
{
|
||||
"pkg": "postcss",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-6g55-p6wh-862q",
|
||||
"title": "PostCSS: Arbitrary file read and information disclosure via attacker-controlled sourceMappingURL in CSS comments",
|
||||
"range": "<=8.5.11"
|
||||
},
|
||||
{
|
||||
"pkg": "postcss",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-fxqj-rqcc-2cmp",
|
||||
"title": "PostCSS: incomplete fix of GHSA-6g55-p6wh-862q — attacker-controlled sourceMappingURL reads arbitrary .map files when `from` is unset",
|
||||
"range": "<=8.5.22"
|
||||
},
|
||||
{
|
||||
"pkg": "postcss",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-qx2v-qp2m-jg93",
|
||||
"title": "PostCSS has XSS via Unescaped </style> in its CSS Stringify Output",
|
||||
"range": "<8.5.10"
|
||||
},
|
||||
{
|
||||
"pkg": "postcss",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-r28c-9q8g-f849",
|
||||
"title": "PostCSS: Path Traversal in Previous Source Map Auto-Loading (sourceMappingURL) leads to Arbitrary .map File Disclosure",
|
||||
"range": "<=8.5.17"
|
||||
},
|
||||
{
|
||||
"pkg": "probe-image-size",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-gjj5-9665-rwrc",
|
||||
"title": "probe-image-size: Quadratic-time Denial of Service in the SVG Parser",
|
||||
"range": "<=7.3.0"
|
||||
},
|
||||
{
|
||||
"pkg": "serialize-javascript",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-5c6j-r48x-rmvq",
|
||||
"title": "Serialize JavaScript is Vulnerable to RCE via RegExp.flags and Date.prototype.toISOString()",
|
||||
"range": "<=7.0.2"
|
||||
},
|
||||
{
|
||||
"pkg": "serialize-javascript",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-m5mm-p8h7-m29v",
|
||||
"title": "Serialize JavaScript: Cross-site scripting (XSS) via unescaped </script at the end of a serialized function body",
|
||||
"range": "<7.1.3"
|
||||
},
|
||||
{
|
||||
"pkg": "serialize-javascript",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-qj8w-gfj5-8c6v",
|
||||
"title": "Serialize JavaScript has CPU Exhaustion Denial of Service via crafted array-like objects",
|
||||
"range": ">=5.0.0 <7.0.5"
|
||||
},
|
||||
{
|
||||
"pkg": "sharp",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-f88m-g3jw-g9cj",
|
||||
"title": "sharp inherited vulnerabilities in libvips: CVE-2026-33327, CVE-2026-33328, CVE-2026-35590, CVE-2026-35591",
|
||||
"range": "<0.35.0"
|
||||
},
|
||||
{
|
||||
"pkg": "sharp",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-rgj7-g3m4-5g8c",
|
||||
"title": "sharp: Vulnerabilities in libheif: GHSA-g89c-p67h-r497 and GHSA-2jg2-4ch7-h545",
|
||||
"range": "<0.35.4"
|
||||
},
|
||||
{
|
||||
"pkg": "sharp",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-wq5f-xc86-pv6w",
|
||||
"title": "sharp : Vulnerability in librsvg dependency CVE-2026-96889",
|
||||
"range": "<0.35.5"
|
||||
},
|
||||
{
|
||||
"pkg": "socket.io-parser",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-2m8v-j782-fhvr",
|
||||
"title": "Socket.IO: Zero-attachment Memory Exhaustion",
|
||||
"range": ">=4.0.0 <4.2.7"
|
||||
},
|
||||
{
|
||||
"pkg": "socket.io-parser",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-677m-j7p3-52f9",
|
||||
"title": "socket.io allows an unbounded number of binary attachments",
|
||||
"range": ">=4.0.0 <4.2.6"
|
||||
},
|
||||
{
|
||||
"pkg": "source-map-js",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-68fv-2mgg-jv7q",
|
||||
"title": "source-map-js allows event-loop denial of service through indexed source-map section offsets",
|
||||
"range": ">=1.0.0 <1.2.2"
|
||||
},
|
||||
{
|
||||
"pkg": "svgo",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-2p49-hgcm-8545",
|
||||
"title": "SVGO removeScripts plugin leaves some executable scripts intact",
|
||||
"range": ">=1.0.0 <2.8.3"
|
||||
},
|
||||
{
|
||||
"pkg": "svgo",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-4vpr-x523-8j87",
|
||||
"title": "SVGO: removeScripts incompletely sanitizes executable HTML in SVG foreignObject elements",
|
||||
"range": ">=1.0.0 <2.8.4"
|
||||
},
|
||||
{
|
||||
"pkg": "svgo",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-w27v-7q3p-w38r",
|
||||
"title": "SVGO: removeScripts allows executable links through namespace and control-character bypasses",
|
||||
"range": ">=1.0.0 <2.8.4"
|
||||
},
|
||||
{
|
||||
"pkg": "svgo",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-xpqw-6gx7-v673",
|
||||
"title": "SVGO DoS through entity expansion in DOCTYPE (Billion Laughs)",
|
||||
"range": ">=2.1.0 <2.8.1"
|
||||
},
|
||||
{
|
||||
"pkg": "tmp",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-52f5-9888-hmc6",
|
||||
"title": "tmp allows arbitrary temporary file / directory write via symbolic link `dir` parameter",
|
||||
"range": "<=0.2.3"
|
||||
},
|
||||
{
|
||||
"pkg": "tmp",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-ph9p-34f9-6g65",
|
||||
"title": "tmp has Path Traversal via unsanitized prefix/postfix that enables directory escape",
|
||||
"range": "<0.2.6"
|
||||
},
|
||||
{
|
||||
"pkg": "webpack-dev-middleware",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-g84c-rxfj-3j2c",
|
||||
"title": "webpack-dev-middleware vulnerable to Path Traversal via non-slash-terminated publicPath",
|
||||
"range": "<7.4.5"
|
||||
},
|
||||
{
|
||||
"pkg": "ws",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-58qx-3vcg-4xpx",
|
||||
"title": "ws: Uninitialized memory disclosure",
|
||||
"range": ">=8.0.0 <8.20.1"
|
||||
},
|
||||
{
|
||||
"pkg": "ws",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-96hv-2xvq-fx4p",
|
||||
"title": "ws: Memory exhaustion DoS from tiny fragments and data chunks",
|
||||
"range": ">=8.0.0 <8.21.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Audit gate: fail the build on any advisory that is not a tracked exception.
|
||||
*
|
||||
* `npm audit --audit-level=<x>` is all-or-nothing, and this tree cannot pass a
|
||||
* flat gate: the Gatsby 5 build toolchain carries ~100 advisories at
|
||||
* low/moderate/high with no non-breaking fix, because gatsby is a runtime
|
||||
* dependency of the build. Blocking on any of those stops publishing
|
||||
* permanently; ignoring the tree lets a genuinely new advisory through. So the
|
||||
* known set is committed to audit-baseline.json with an owner and a reason,
|
||||
* and this script fails only when something NEW appears at or above the
|
||||
* threshold.
|
||||
*
|
||||
* Usage: node scripts/audit-gate.mjs [--write-baseline]
|
||||
*/
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const BASELINE = join(HERE, 'audit-baseline.json');
|
||||
const BLOCKING = new Set(['critical', 'high']);
|
||||
|
||||
/** npm audit exits non-zero when it finds anything, so ignore the status. */
|
||||
function auditReport() {
|
||||
let out;
|
||||
try {
|
||||
out = execFileSync('npm', ['audit', '--json', '--omit=optional'], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
maxBuffer: 32 * 1024 * 1024
|
||||
});
|
||||
} catch (err) {
|
||||
out = err.stdout;
|
||||
if (!out) throw err;
|
||||
}
|
||||
return JSON.parse(out);
|
||||
}
|
||||
|
||||
/** Flatten the report to one key per package/severity/advisory triple. */
|
||||
function currentFindings(report) {
|
||||
const findings = new Map();
|
||||
for (const [pkg, v] of Object.entries(report.vulnerabilities ?? {})) {
|
||||
if (!BLOCKING.has(v.severity)) continue;
|
||||
const advisories = (v.via ?? []).filter((x) => typeof x === 'object');
|
||||
if (advisories.length === 0) {
|
||||
// via is a chain of parent package names - attribute to the package.
|
||||
findings.set(`${pkg}|${v.severity}|transitive`, { pkg, severity: v.severity, advisory: 'transitive' });
|
||||
continue;
|
||||
}
|
||||
for (const a of advisories) {
|
||||
const id = (a.url ?? '').split('/').pop() || a.title || 'unknown';
|
||||
findings.set(`${pkg}|${v.severity}|${id}`, { pkg, severity: v.severity, advisory: id, title: a.title, range: a.range });
|
||||
}
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
const report = auditReport();
|
||||
const current = currentFindings(report);
|
||||
|
||||
if (process.argv.includes('--write-baseline')) {
|
||||
const accepted = [...current.values()].sort((a, b) => `${a.pkg}${a.advisory}`.localeCompare(`${b.pkg}${b.advisory}`));
|
||||
writeFileSync(
|
||||
BASELINE,
|
||||
JSON.stringify(
|
||||
{
|
||||
$comment:
|
||||
'Advisories at critical/high accepted for this tree. scripts/audit-gate.mjs fails the build on anything not listed here, so removing an entry re-arms the gate for that advisory. Regenerate with: node scripts/audit-gate.mjs --write-baseline',
|
||||
owner: 'Site maintainers (allan@4gl.io)',
|
||||
reason:
|
||||
'Gatsby 5 build toolchain: no non-breaking fix available upstream, and these are build-time dependencies of a statically generated site. Re-check on each Gatsby major bump.',
|
||||
accepted
|
||||
},
|
||||
null,
|
||||
2
|
||||
) + '\n'
|
||||
);
|
||||
console.log(`wrote ${accepted.length} accepted advisories to ${BASELINE}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const baseline = JSON.parse(readFileSync(BASELINE, 'utf8'));
|
||||
const accepted = new Set((baseline.accepted ?? []).map((a) => `${a.pkg}|${a.severity}|${a.advisory}`));
|
||||
|
||||
const fresh = [...current.entries()].filter(([key]) => !accepted.has(key));
|
||||
const resolved = [...accepted].filter((key) => !current.has(key));
|
||||
|
||||
for (const [, f] of fresh) {
|
||||
console.error(`NEW ${f.severity.toUpperCase()}: ${f.pkg} - ${f.title ?? f.advisory} (${f.advisory})`);
|
||||
}
|
||||
|
||||
if (resolved.length > 0) {
|
||||
// Not a failure: a bumped or removed dependency closes advisories, and the
|
||||
// baseline should be trimmed in the same change that closes them.
|
||||
console.log(`${resolved.length} baseline entries no longer present - consider re-running with --write-baseline:`);
|
||||
for (const key of resolved) console.log(` stale: ${key}`);
|
||||
}
|
||||
|
||||
if (fresh.length > 0) {
|
||||
console.error(`\n${fresh.length} untracked advisory/ies at critical or high.`);
|
||||
console.error('Fix it, or add it to scripts/audit-baseline.json with a reason and an owner.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`audit gate: no new critical/high advisories (${current.size} tracked in baseline)`);
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
import { FaShieldAlt } from 'react-icons/fa'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const StyledCertBadge = styled.a`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
border: 2px solid #314351;
|
||||
border-radius: 0.25rem;
|
||||
color: #314351;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: #314351;
|
||||
}
|
||||
`
|
||||
|
||||
const CertBadge = () => (
|
||||
<StyledCertBadge
|
||||
href="https://sasapps.io/cyber-essentials-certified/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<FaShieldAlt /> Cyber Essentials Certified
|
||||
</StyledCertBadge>
|
||||
)
|
||||
|
||||
export default CertBadge
|
||||
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 482 KiB |
@@ -5,6 +5,7 @@ import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
|
||||
import { Section } from '../components/shared'
|
||||
import CertBadge from '../components/shared/certBadge'
|
||||
import {
|
||||
SectionHeading,
|
||||
SectionDesc
|
||||
@@ -37,6 +38,7 @@ const About: React.FC<PageProps<unknown>> = ({ location }) => {
|
||||
</a>
|
||||
.
|
||||
</SectionDesc>
|
||||
<CertBadge />
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
@@ -7,6 +7,7 @@ import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
|
||||
import { Section, ScheduleDemo } from '../components/shared'
|
||||
import CertBadge from '../components/shared/certBadge'
|
||||
import {
|
||||
SectionHeading,
|
||||
SectionDesc
|
||||
@@ -98,6 +99,7 @@ const Home: React.FC<PageProps<IndexPageData>> = ({ data, location }) => {
|
||||
perform manual data uploads into their preferred database, in
|
||||
real-time, with full validation, approval, security, and control.
|
||||
</SectionDesc>
|
||||
<CertBadge />
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<Art src={rightArt} info="Clinical Research Data" />
|
||||
|
||||
@@ -118,6 +118,22 @@ const StyledContent = styled.div`
|
||||
line-height: 2em;
|
||||
font-family: Monaco, 'Andale Mono', 'Courier New', Courier, monospace;
|
||||
}
|
||||
/* A fenced block tagged with the 'poem' language is verse, not code: drop
|
||||
the code chrome and set it in the serif face, keeping the author's line
|
||||
breaks and blank lines (which read as stanza breaks). */
|
||||
pre:has(> code.language-poem) {
|
||||
background-image: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 1.8rem 0;
|
||||
line-height: 1.75;
|
||||
}
|
||||
pre > code.language-poem {
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: 1.05rem;
|
||||
color: #222222;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
`
|
||||
|
||||
interface PostProps {
|
||||
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>One Table, Two Rule Sets has moved</title>
|
||||
<link rel="canonical" href="https://docs.datacontroller.io/developer-guide/two-rule-sets-one-table/" />
|
||||
<meta http-equiv="refresh" content="0; url=https://docs.datacontroller.io/developer-guide/two-rule-sets-one-table/" />
|
||||
<meta name="robots" content="noindex" />
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
This page has moved to
|
||||
<a href="https://docs.datacontroller.io/developer-guide/two-rule-sets-one-table/">docs.datacontroller.io</a>.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||