A web app that connects to relational databases over JDBC and shows their metadata: tables, columns, primary/foreign keys, indexes, views, stored procedures, and the generated CREATE TABLE DDL. Admins register connection profiles per engine; users with appropriate roles browse, annotate, tag, glossary-link, and chart cross-database data lineage.
Supported engines for browsing: Oracle, MySQL, PostgreSQL, Netezza, IBM DB2, MariaDB, Microsoft SQL Server, Teradata, Vertica, Elasticsearch SQL, H2 — 11 engines, all driven from JDBC jars in backend/driver/.
Key features:
- Annotation overlay — edit table/column descriptions in-app, preserve the original source comment, push back to the source DB on admin approval.
- Tags — tag tables or individual columns; colour-coded chips throughout the UI.
- Business glossary — shared term definitions linked to specific columns.
- Data lineage — directed graph of
source → targetrelationships between tables, including cross-database edges (e.g. a Postgres OLTP table feeding an Oracle warehouse). - Schema snapshots — fingerprint a schema's structure and detect added / removed / changed tables on the next capture.
- API tokens —
Authorization: Bearer mm_…for programmatic clients alongside the SPA's cookie auth. - Recycle bin — soft-delete connections; restore or purge within the bin.
- i18n — English and Vietnamese; one-click toggle in the sidebar.
- Admin tooling — push approvals, activity monitor, user groups, LDAP group-to-role mappings, webhooks, active sessions list.
Stack:
- Backend: Spring Boot 3 (Java 17), Maven; Oracle for the app's own persistence; per-engine JDBC drivers for browsing user-registered databases; Spring Security with Active Directory LDAP for login (plus API-token bearer auth and a bootstrap-admin escape hatch).
- Frontend: React 18, Vite, TypeScript, Tailwind CSS v3, Zustand, React Router 6, ApexCharts, @xyflow/react (ERD + lineage graphs), react-i18next, react-hot-toast, Boxicons.
-
JDK 17+
-
Maven 3.9+
-
Node.js 18+ and npm
-
An Oracle 12c+ database for the app itself — schema with
CREATE TABLEgrants. The app will auto-create its tables on first run viaddl-auto=update. The full list:Table Purpose database_connectionSaved connection profiles (with deleted_atfor soft-delete)metadata_annotation+metadata_annotation_historyOverlay descriptions and an audit trail role_assignmentPer-user role assignments user_group+group_membershipCustom groups with inheritable roles ldap_config+ldap_group_mappingLDAP settings + group → role mapping bootstrap_admin_credentialHashed break-glass-admin password login_eventSuccessful sign-in history (powers the activity monitor) audit_eventAppend-only audit trail of admin actions push_requestEditor-submitted push approvals webhook_configOutbound webhooks for push events tag+table_tagPer-table / per-column tags glossary_term+glossary_linkBusiness glossary + column links api_tokenProgrammatic access tokens (SHA-256 hashed) schema_snapshotDDL fingerprints for drift detection lineage_edgeDirected lineage between tables (including cross-DB) -
Network access to the source databases you want to browse (registered later from the UI).
Open two terminals.
Terminal 1 — backend (port 8080):
cd backend
mvn spring-boot:runTerminal 2 — frontend (port 5173):
cd frontend
npm install
npm run devOpen http://localhost:5173. On a fresh install, the first-run setup wizard appears once to configure the app's database — see the section below. It never shows again after the data source has been saved.
The Vite dev server proxies /api/* to http://localhost:8080, so CORS is not exercised during development.
For UI work or demos without a real backend, the repo ships an Express-based mock that mirrors every endpoint:
cd frontend
npm run dev:mock # starts both: mock API on :8080 + Vite on :5173Login with any credentials. Usernames containing viewer → VIEWER, editor → EDITOR, anything else → ADMIN. Seven connections come pre-seeded (one per engine family) along with sample overlay annotations and pending push requests so the Approvals + Preview flows show real content immediately.
This wizard runs exactly once — the first time the app is started before the app's Oracle database has been configured. Once the data source is saved, the wizard never appears again.
Before any login is possible, the backend must be pointed at its own Oracle database. When the backend starts for the first time — before app-data/datasource.properties has been written — it falls back to an in-memory H2 database so it can serve the setup wizard.
The frontend detects this by calling GET /api/setup/status on load. If the response shows configured: false or reachable: false, the setup wizard replaces the whole app. The wizard lets you:
- Enter Oracle JDBC connection details (URL, username, password).
- Click Test connection to verify they work.
- Click Save — writes
app-data/datasource.propertiesnext to the jar and restarts the Spring datasource in-process.
After a successful save, the backend reconnects to Oracle, creates the schema tables automatically, and the login screen appears.
Login is blocked with HTTP 503 until the file exists — this prevents bypassing the wizard via curl.
Production note: the setup endpoints (
/api/setup/*) are intentionally unauthenticated. Bind the backend to localhost or put it behind a VPN/reverse proxy during initial setup on public infrastructure.
Alternatively, skip the wizard entirely by supplying environment variables (see App DB configuration) before the first run — the wizard won't appear if the file already exists or the env vars point at a reachable Oracle instance.
The app writes its Oracle connection details to app-data/datasource.properties via the setup wizard. You can also point the app at its Oracle DB directly via environment variables — these override the defaults in application.yml and take precedence over any wizard-saved file:
| Env var | Example | Notes |
|---|---|---|
APP_DB_URL |
jdbc:oracle:thin:@//db.example.com:1521/ORCLPDB1 |
Service-name form. Use jdbc:oracle:thin:@host:port:SID for SID-based DBs. |
APP_DB_USERNAME |
metadata_app |
Schema owner; needs CREATE TABLE grants. |
APP_DB_PASSWORD |
s3cret |
If neither env vars nor the wizard file are present, the backend starts with an in-memory H2 fallback and shows the setup wizard on first load.
The app ships a built-in local administrator account that authenticates without LDAP — the "bootstrap admin". Its purpose is to let you sign in and configure LDAP even when Active Directory is unreachable, and to give you a way in before any AD users have been assigned roles.
| Env var | Default | Notes |
|---|---|---|
APP_BOOTSTRAP_ADMIN_USERNAME |
admin |
Username for the break-glass account. Set to "" to disable entirely. |
APP_BOOTSTRAP_ADMIN_PASSWORD |
admin |
Change this in production. |
The bootstrap admin is always granted ADMIN role (it is included in APP_DEFAULT_ADMINS). It does not go through LDAP — credentials are verified against the value stored by BootstrapAdminService (initially from application.yml, then from the app's Oracle DB once a custom password has been saved).
Changing the password: the bootstrap admin has a Change password link at the bottom of the admin sidebar. Only the bootstrap admin (superAdmin: true in GET /api/auth/me) can reach that page — other ADMIN users get a 403. The current password must be supplied to set a new one.
The bootstrap admin shows as superAdmin: true in the API and UI. Regular AD admins are superAdmin: false.
The app requires sign-in before any /api/* endpoint can be called. Regular users sign in with Active Directory credentials. The bootstrap admin (see above) can also sign in without LDAP.
LDAP settings are configured at runtime via the LDAP Config page in the admin sidebar — they are stored in the ldap_config table in the app's Oracle DB and take effect immediately without a restart. Environment variables serve as a production override if the DB row is unavailable.
| Env var | Example | Notes |
|---|---|---|
APP_LDAP_URL |
ldap://dc01.corp.example.com:389 |
Use ldaps://...:636 for TLS |
APP_LDAP_DOMAIN |
corp.example.com |
Username is bound as user@DOMAIN |
APP_LDAP_ROOT_DN |
DC=corp,DC=example,DC=com |
Optional; blank lets the provider derive it from the domain |
APP_LDAP_USER_SEARCH_FILTER |
(&(objectClass=user)(sAMAccountName={0})) |
Default — works for standard AD |
APP_DEFAULT_ADMINS |
alice,bob |
Comma-separated AD usernames always granted ADMIN, even with no DB row |
Sessions are stored in a cookie (JSESSIONID). There is no JWT, no local user table, and no signup flow — accounts live entirely in AD (except the bootstrap admin).
Sessions expire automatically after 20 minutes of inactivity — any mouse movement, keypress, or scroll resets the timer. The Spring backend's server.servlet.session.timeout matches so the cookie and the SPA's countdown expire together.
If /api/auth/me returns 401 mid-session, the SPA automatically falls back to the login screen.
For automation, scripts, or external services, generate an API token from the API Tokens page in the sidebar (Settings → API Tokens). Each token is granted a fixed role (VIEWER, EDITOR, or ADMIN) and an optional expiry. The raw secret (mm_…) is shown once on creation — copy it immediately; it's not retrievable later.
Pass it as a bearer header on any /api/* call instead of a session cookie:
Authorization: Bearer mm_<token>
Bearer auth and session auth coexist — programmatic clients don't need to call /api/auth/login first.
The SPA uses React Router 6 with browser history — every page is bookmarkable, back/forward works, and refreshing keeps you on the same screen.
| URL | Page | Required role |
|---|---|---|
/ |
Empty state with "Manage connections" CTA | any signed-in user |
/connections |
Connections management (table, filter, edit, delete) | any signed-in user (CRUD is admin-only) |
/browse/:id |
Metadata explorer (schemas → tables → details → DDL → ERD) | any signed-in user |
/tags |
Tag catalog | any signed-in user (CRUD is editor/admin) |
/glossary |
Business glossary | any signed-in user (CRUD is editor/admin) |
/lineage |
Data lineage graph (cross-DB capable) | any signed-in user (CRUD is editor/admin) |
/compare |
Schema compare (side-by-side diff) | any signed-in user |
/monitoring |
Activity Monitor | ADMIN |
/approvals |
Push request approvals with diff + DDL preview | ADMIN |
/sessions |
Active sessions list (force sign-out) | ADMIN |
/recycle-bin |
Soft-deleted connections (restore / purge) | ADMIN |
/roles |
Users & Roles | ADMIN |
/groups |
User Groups | ADMIN |
/ldap-config |
LDAP configuration (runtime, stored in DB) | ADMIN |
/webhooks |
Outbound webhook configuration | ADMIN |
/api-tokens |
Programmatic API token management | ADMIN |
/change-password |
Change bootstrap-admin password | super-admin only |
Visiting an admin-only URL as a non-admin redirects to /. Visiting any URL while anonymous shows the login screen; after sign-in the user lands at the URL they originally requested.
Three roles in increasing capability:
| Role | What they can do |
|---|---|
| VIEWER | Browse metadata (tables, columns, keys, indexes, views, procedures, generated DDL). Default for any authenticated user with no assignment. |
| EDITOR | Everything VIEWER can do, plus edit table/column descriptions and submit push requests for admin approval. Cannot push directly. |
| ADMIN | Everything EDITOR can do, plus create/edit/delete connection profiles, approve or reject push requests, push to source DB directly, and manage role assignments. |
Assignments are managed from the Users & Roles page in the app (admins only). Usernames listed in APP_DEFAULT_ADMINS are always treated as ADMIN regardless of any DB row, so you can't lock yourself out by deleting every assignment — set at least one trusted username there.
Connections appear in two places:
- Sidebar "Recent" — the last 6 connections you've actually browsed (per-user, persisted to
localStorage). Empty until you visit one. - Manage Connections (
/connections) — full table view of every saved profile with filter, click-to-browse, and admin-only New / Edit / Delete buttons.
Connection profiles (host, port, service name / SID / database, username, password) are persisted in the app's own Oracle database (database_connection table). Passwords are encrypted at rest with AES-GCM (256-bit) before being written.
Allowed schemas — each connection can optionally carry an allowedSchemas list. When non-empty, only the listed schema names are offered in the schema picker, regardless of what the database user can see. Leave it empty to expose all visible schemas.
The AES key is read from app.security.encryption-key in application.yml, which can be overridden with the APP_ENCRYPTION_KEY environment variable. The default key in application.yml is a placeholder — generate your own for any non-local use:
# 32 random bytes, Base64 encoded — paste into APP_ENCRYPTION_KEY
[Convert]::ToBase64String([byte[]](1..32 | ForEach-Object { Get-Random -Maximum 256 }))If you change the key after saving connections, previously stored passwords cannot be decrypted — truncate database_connection in the app DB and re-create the connections.
Admins can divide users into custom groups from the User Groups page in the sidebar. A group has a name, an optional description, and an optional inherited role.
- Purely organisational group (no role) — useful for tagging "Marketing Analysts" or "Data Platform Team"; no permission effect.
- Group with a role — every member of the group inherits that role on top of any explicit per-user assignment they have. Effective role on each sign-in is
max(explicit, every group role)using the orderingVIEWER < EDITOR < ADMIN.
Group membership can only raise a user's role, never lower it — there is no deny rule. Changes take effect on the affected user's next sign-in.
Admins get an Activity Monitor page from the sidebar that shows:
- Five summary stat cards — total logins all-time, distinct users all-time, annotated tables, logins in the last 30 days, and table edits in the last 30 days.
- Users logged in (last 30 days) — daily bar chart. Primary bars are distinct users per day; lighter overlay bars are total login events per day.
- Tables with updated metadata (last 30 days) — daily bar chart of distinct
(connection, schema, table)tuples touched. - Logins per user — total login count and last-seen timestamp, sorted by busiest user first.
- Recent logins — the last ~25 successful sign-in events with username and client IP.
- Tables with changing metadata — every table that has at least one overlay annotation, ordered by most recently edited.
Daily buckets use Oracle TRUNC() in the DB session timezone (not UTC), so the chart's "day" is a day on the database server's clock.
Login events are written on every successful sign-in (AD or bootstrap admin) into the login_event table. Failed logins are not stored; this is for usage reporting, not intrusion detection.
When an admin clicks + New Connection they pick the engine. The form adapts: default port auto-fills, the database field is labelled appropriately, and the Oracle-only SID/Service Name toggle only appears for Oracle.
| Engine | Driver jar (in backend/driver/) |
Default port | What "database" means | Comment push DDL |
|---|---|---|---|---|
| Oracle | ojdbc17.jar |
1521 | Service name or SID | COMMENT ON TABLE/COLUMN <schema>.<table>[.<column>] IS '…' |
| MySQL | mysql-connector-j-9.7.0.jar |
3306 | Database name | ALTER TABLE <schema>.<table> COMMENT = '…' / MODIFY COLUMN <name> <type> COMMENT '…' |
| PostgreSQL | postgresql-42.7.11.jar |
5432 | Database name | Same COMMENT ON as Oracle |
| Netezza | nzjdbc3.jar |
5480 | Database name | COMMENT ON TABLE <schema>.<table>; COMMENT ON COLUMN <table>.<column> (no schema prefix) |
| IBM DB2 | jcc-11.5.9.0.jar |
50000 | Database name | COMMENT ON TABLE/COLUMN … |
| MariaDB | mariadb-java-client-3.3.2.jar |
3306 | Database name | Same as MySQL (ALTER TABLE … COMMENT =) |
| SQL Server | mssql-jdbc-12.8.0.jre11.jar |
1433 | Database name | EXEC sp_addextendedproperty 'MS_Description', N'…', 'SCHEMA', N'…', 'TABLE', N'…'[, 'COLUMN', N'…'] |
| Teradata | terajdbc-20.00.00.06.jar |
1025 | Database (= schema) | COMMENT ON TABLE/COLUMN … IS '…' |
| Vertica | vertica-jdbc-23.3.0-0.jar |
5433 | Database name | COMMENT ON TABLE/COLUMN … IS '…' |
| Elasticsearch | x-pack-sql-jdbc-8.12.1.jar |
9200 | (unused — indices are flat) | Read-only; cannot push comments |
| H2 | h2-legacy.jar |
9092 | Database name | COMMENT ON TABLE/COLUMN … IS '…' |
All JDBC drivers — including Oracle/MySQL/Postgres — are sourced from backend/driver/ as system-scoped Maven dependencies. The build is self-contained and doesn't reach out to Maven Central for any driver. See backend/driver/README.md for the rationale and the upgrade procedure.
To replace a driver:
- Drop the new jar into
backend/driver/. - Update the matching
<systemPath>inbackend/pom.xml(and the<version>if you care — it's cosmetic for system-scoped deps). - Rebuild:
mvn spring-boot:runfor dev,mvn clean packagefor prod.
The spring-boot-maven-plugin is configured with <includeSystemScope>true</includeSystemScope>, so the jars land in BOOT-INF/lib of the fat-jar automatically.
backend/driver/*.jar is gitignored — the licensed binaries stay on your machine. The folder and its README are tracked so a fresh clone has the convention in place.
Oracle uses a native extractor (OracleMetadataExtractor) that hits ALL_* dictionary views and returns column defaults, view bodies, procedure validity, and uses DBMS_METADATA.GET_DDL for table DDL.
PostgreSQL (PostgresMetadataExtractor) and MySQL (MySqlMetadataExtractor) have native extractors that extend the generic base to return view definition bodies (from information_schema.views) and apply engine-correct schema casing (lowercase pass-through instead of Oracle-style uppercasing).
All other engines use the generic DatabaseMetaData-based extractor — fast and portable, with the table DDL constructed from the standard JDBC metadata. To add native fidelity for any engine, drop a new MetadataExtractor @Component next to the others in service/metadata/ and the dispatcher picks it up automatically.
Descriptions for tables and individual columns can be edited inline in the UI. Edits are stored as an overlay in the app's own Oracle database, keyed by (connection, schema, table, column). The source database is never modified by an edit — the overlay is layered on top of live metadata when the table is viewed.
Edited fields are marked with an (overlay) badge, and the original source comment is preserved so you can see what was there before. Use Clear overlay to drop the edit and fall back to the source.
To get overlay descriptions written into the source database, the path depends on role:
- EDITOR clicks Request push approval → creates a
PENDINGrow inpush_request. Their overlays stay editable until an admin acts. They cannot push directly (/annotations/pushis ADMIN-only). - ADMIN can either click Push to source DB for an immediate push, or open Push Approvals to review and approve pending requests one at a time.
The Push Approvals page shows all pending requests. Clicking any cell in a row opens a preview dialog that:
- Fetches the merged table detail and shows a Before / After diff of every overlay annotation (table-level + per column).
- Calls a dry-run endpoint that builds the exact engine-correct DDL the source DB would receive —
COMMENT ON …for the Oracle family,ALTER TABLE … COMMENT =for MySQL/MariaDB,sp_addextendedpropertyfor SQL Server, etc. - Lists any targets that can't be pushed (e.g. an Elasticsearch column comment) under a separate "Will be skipped" section.
Admins Approve to execute the push and close the request, or Reject to close it without touching the source DB. Pending count surfaces as a badge on the sidebar nav. Partial push failures are stored on the request row in the push_errors column.
Identifiers are validated against an engine-portable safe-identifier regex (^[A-Za-z_][A-Za-z0-9_$#]{0,127}$) before being inlined into DDL. Description literals are escaped by doubling single quotes. The identifier check is the only injection guard — descriptions can never be parameterised in DDL, so do not relax it.
Sidebar has a sun/moon toggle (also available on the login screen, top-right). Choice is persisted to localStorage; defaults to the OS-level prefers-color-scheme on first load. Colors are driven by CSS variables (--bg-rgb, --fg-rgb, --accent-rgb, …) so charts, toasts, code blocks, and custom scrollbars all flip in sync.
cd frontend; npm run build # outputs to frontend/dist
cd ../backend; mvn clean package # outputs backend/target/*.jar
java -jar backend/target/metadata-management-0.0.1-SNAPSHOT.jarCopy the SPA build into Spring's static resources before packaging — the resulting fat-jar serves both the API and the SPA on port 8080:
cd frontend; npm run build
cp -r dist/* ../backend/src/main/resources/static/
cd ../backend; mvn clean package
java -jar target/metadata-management-0.0.1-SNAPSHOT.jarSpaForwardController forwards every non-API, non-asset GET to /index.html, so a hard refresh on routes like /browse/42 or /lineage works without needing a separate reverse-proxy rewrite rule.
Serve frontend/dist from any static host or a reverse proxy in front of Spring Boot. Make sure your host falls back to index.html for unknown paths so React Router routes work on refresh. Set APP_CORS_ALLOWED_ORIGINS to the SPA's origin so the auth cookie flows.
All /api/* endpoints require an authenticated session, except /api/auth/login and the /api/setup/* group.
| Method | Path | Notes |
|---|---|---|
| GET | /api/setup/status |
Returns {configured, reachable} — drives the first-run wizard |
| POST | /api/setup/datasource/test |
Test-connect with the supplied credentials before saving |
| POST | /api/setup/datasource |
Save credentials to app-data/datasource.properties and reload the datasource |
| Method | Path | Notes |
|---|---|---|
| POST | /api/auth/login |
Body {username, password} — authenticates against AD or as bootstrap admin |
| POST | /api/auth/logout |
Clears the session cookie |
| GET | /api/auth/me |
Returns {username, role, superAdmin} or 401 |
| Method | Path | Notes |
|---|---|---|
| GET | /api/connections |
List active (non-deleted) connections |
| POST | /api/connections |
Create connection (ADMIN) |
| PUT | /api/connections/{id} |
Update connection (ADMIN) |
| DELETE | /api/connections/{id} |
Soft-delete — sets deleted_at; row stays recoverable via the recycle bin (ADMIN) |
| GET | /api/connections/recycle-bin |
List soft-deleted connections (ADMIN) |
| PUT | /api/connections/{id}/restore |
Restore a soft-deleted connection (ADMIN) |
| DELETE | /api/connections/{id}/purge |
Permanently delete a connection + all dependent rows (ADMIN) |
| POST | /api/connections/test |
Test ad-hoc credentials (ADMIN) |
| POST | /api/connections/{id}/test |
Test saved connection (ADMIN) |
| Method | Path | Notes |
|---|---|---|
| GET | /api/connections/{id}/metadata/schemas |
List schemas visible to the connection user |
| GET | /api/connections/{id}/metadata/tables?schema= |
List tables in a schema |
| GET | /api/connections/{id}/metadata/tables/{tableName}?schema= |
Columns, PK, FKs, indexes |
| GET | /api/connections/{id}/metadata/tables/{tableName}/ddl?schema= |
Generated CREATE TABLE DDL + indexes + COMMENT ON … for every annotated row |
| GET | /api/connections/{id}/metadata/views?schema= |
Views and their text |
| GET | /api/connections/{id}/metadata/procedures?schema= |
Procedures, functions, packages |
| Method | Path | Notes |
|---|---|---|
| GET | /api/connections/{id}/annotations?schema=&tableName= |
List overlay annotations for a table |
| PUT | /api/connections/{id}/annotations |
Upsert one annotation (EDITOR/ADMIN) |
| DELETE | /api/connections/{id}/annotations?schema=&tableName=&columnName= |
Remove an overlay (EDITOR/ADMIN); columnName omitted ⇒ table-level |
| POST | /api/connections/{id}/annotations/push?schema=&tableName= |
ADMIN only — push overlays to source DB directly |
| GET | /api/connections/{id}/annotations/push/preview?schema=&tableName= |
Dry-run: returns the exact statements the source DB would receive (EDITOR/ADMIN) |
| Method | Path | Notes |
|---|---|---|
| POST | /api/push-requests |
Submit an approval request (EDITOR/ADMIN). Body {connectionId, schemaName, tableName, requestNote?} |
| GET | /api/push-requests?status=pending|all |
List push requests (ADMIN only) |
| GET | /api/push-requests/pending-count |
Sidebar badge count (ADMIN only) |
| POST | /api/push-requests/{id}/approve |
Approve and execute the push (ADMIN only). Optional body {reviewerNote} |
| POST | /api/push-requests/{id}/reject |
Reject without pushing (ADMIN only). Optional body {reviewerNote} |
| Method | Path | Notes |
|---|---|---|
| GET | /api/roles |
List role assignments (ADMIN only) |
| PUT | /api/roles |
Upsert one assignment (ADMIN only) |
| DELETE | /api/roles/{username} |
Remove an assignment (ADMIN only) |
| Method | Path | Notes |
|---|---|---|
| GET | /api/groups |
List custom user groups (ADMIN only) |
| PUT | /api/groups |
Create or update a group (ADMIN only) |
| DELETE | /api/groups/{id} |
Delete a group and its memberships (ADMIN only) |
| GET | /api/groups/{id}/members |
List members of a group (ADMIN only) |
| POST | /api/groups/{id}/members |
Add a user; body {"username": "…"} (ADMIN only) |
| DELETE | /api/groups/{id}/members/{username} |
Remove a user from a group (ADMIN only) |
| Method | Path | Notes |
|---|---|---|
| GET | /api/monitoring/login-stats |
Per-user login counts and last seen (ADMIN only) |
| GET | /api/monitoring/recent-logins?limit= |
Most recent login events (ADMIN only) |
| GET | /api/monitoring/annotation-activity?limit= |
Tables with changing metadata (ADMIN only) |
| GET | /api/monitoring/login-trend?days= |
Daily login bucket for the trailing N days, zero-filled (ADMIN only) |
| GET | /api/monitoring/annotation-trend?days= |
Daily count of distinct tables updated, trailing N days (ADMIN only) |
| Method | Path | Notes |
|---|---|---|
| GET | /api/tags |
List tag catalog |
| PUT | /api/tags |
Upsert a tag (EDITOR/ADMIN) |
| DELETE | /api/tags/{id} |
Delete a tag (EDITOR/ADMIN) |
| GET | /api/connections/{id}/tags?schema=&tableName=&columnName= |
Tags assigned to a table (or specific column when columnName is set) |
| GET | /api/connections/{id}/tags/by-column?schema=&tableName= |
All tags on the table, grouped by columnName ("" key = table-level) |
| POST | /api/connections/{id}/tags/{tagId}?schema=&tableName=&columnName= |
Assign a tag (EDITOR/ADMIN) |
| DELETE | /api/connections/{id}/tags/{tagId}?schema=&tableName=&columnName= |
Unassign a tag (EDITOR/ADMIN) |
| Method | Path | Notes |
|---|---|---|
| GET | /api/glossary |
List business terms |
| PUT | /api/glossary |
Upsert a term (EDITOR/ADMIN) |
| DELETE | /api/glossary/{id} |
Delete a term and all its links (EDITOR/ADMIN) |
| GET | /api/connections/{id}/glossary-links?schema=&tableName= |
All glossary links on a table |
| POST | /api/connections/{id}/glossary-links?schema=&tableName=&termId=&columnName= |
Link a term to a column (EDITOR/ADMIN) |
| DELETE | /api/connections/{id}/glossary-links?schema=&tableName=&termId=&columnName= |
Remove a link (EDITOR/ADMIN) |
Edges are directional source → target. The target may live in a different connection and/or schema — pass targetConnectionId and targetSchemaName in the POST body to record a cross-DB edge. Omit them to default to the source's connection/schema.
| Method | Path | Notes |
|---|---|---|
| GET | /api/connections/{id}/lineage?schema= |
All edges where this (connection, schema) is source OR target |
| GET | /api/connections/{id}/lineage/table/{table}?schema= |
Edges where the given table is source OR target |
| POST | /api/connections/{id}/lineage |
Create an edge (EDITOR/ADMIN) |
| DELETE | /api/connections/{id}/lineage/{edgeId} |
Delete an edge (EDITOR/ADMIN) |
| Method | Path | Notes |
|---|---|---|
| POST | /api/connections/{id}/snapshots/capture?schema= |
Capture a new snapshot — returns drift vs. the previous one |
| GET | /api/connections/{id}/snapshots?schema= |
List historical snapshots for a schema |
| Method | Path | Notes |
|---|---|---|
| GET | /api/connections/{id}/metadata/schemas/{schema}/docs |
Markdown export of every table in the schema, including overlay annotations, tags, and glossary links |
| GET | /api/connections/{id}/annotations/export?format=json|csv |
Export overlay annotations for one connection |
| POST | /api/connections/{id}/annotations/import |
Multipart upload — re-imports a previously exported file (EDITOR/ADMIN) |
| Method | Path | Notes |
|---|---|---|
| GET | /api/tokens |
List tokens (raw secret never returned) (ADMIN only) |
| POST | /api/tokens |
Create a token. Body {label, role, expiresAt?} — the raw mm_… secret is returned once in the response (ADMIN only) |
| DELETE | /api/tokens/{id} |
Revoke a token (ADMIN only) |
| Method | Path | Notes |
|---|---|---|
| GET | /api/webhooks |
List configured outbound webhooks (ADMIN only) |
| PUT | /api/webhooks |
Create or update one. Body {name, url, secret?, events, active} — events is ALL or a comma-separated list of PUSH_SUBMITTED, PUSH_APPROVED, PUSH_REJECTED (ADMIN only) |
| DELETE | /api/webhooks/{id} |
Delete (ADMIN only) |
| Method | Path | Notes |
|---|---|---|
| GET | /api/admin/sessions |
All active HTTP sessions (ADMIN only) |
| DELETE | /api/admin/sessions/{sessionId} |
Force-invalidate one session (ADMIN only) |
| Method | Path | Notes |
|---|---|---|
| GET | /api/admin/ldap-config |
Get current LDAP settings (ADMIN only) |
| PUT | /api/admin/ldap-config |
Save LDAP settings and reload the auth provider in-process (ADMIN only) |
| GET | /api/admin/ldap-group-mappings |
List LDAP-group → role mappings (ADMIN only) |
| PUT | /api/admin/ldap-group-mappings |
Upsert a mapping (ADMIN only) |
| DELETE | /api/admin/ldap-group-mappings/{id} |
Remove a mapping (ADMIN only) |
| GET | /api/admin/bootstrap-admin/password/status |
Returns {hasCustomPassword} — signals the default password is still in use (super-admin only) |
| PUT | /api/admin/bootstrap-admin/password |
Change the bootstrap-admin password. Body {currentPassword, newPassword} (super-admin only) |