Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Release to NPM

on:
push:
branches: [main]
workflow_dispatch:

jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check out the code
uses: actions/checkout@v4

- name: Setup Node.js with npm registry
uses: actions/setup-node@v4
with:
node-version: "20"
registry-url: "https://registry.npmjs.org"

- name: Compare local and published versions
id: check
run: |
LOCAL=$(node -p "require('./package.json').version")
PUBLISHED=$(npm view @metabase/database-metadata version 2>/dev/null || echo "none")
echo "local=$LOCAL" >> $GITHUB_OUTPUT
echo "published=$PUBLISHED" >> $GITHUB_OUTPUT
if [ "$LOCAL" != "$PUBLISHED" ]; then
echo "changed=true" >> $GITHUB_OUTPUT
else
echo "changed=false" >> $GITHUB_OUTPUT
fi

- name: Publish to NPM
if: steps.check.outputs.changed == 'true'
run: npm publish --tag latest
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_RELEASE_TOKEN }}

- name: Generate workflow summary
run: |
tee -a $GITHUB_STEP_SUMMARY << EOF
## @metabase/database-metadata

- **Local version**: ${{ steps.check.outputs.local }}
- **Published version**: ${{ steps.check.outputs.published }}
- **Published this run**: ${{ steps.check.outputs.changed }}
EOF
24 changes: 24 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Validate

on:
push:
branches: [main]
pull_request:

jobs:
validate-export:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- run: bun install

- name: Extract metadata
run: bun run bin/cli.js extract-metadata examples/v1/metadata.json /tmp/databases

- name: Diff examples
run: diff -r examples/v1/databases /tmp/databases
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
Thumbs.db
Desktop.ini
*~
*.swp
*.swo
node_modules/
*.tgz
79 changes: 78 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,78 @@
# database-metadata
# Metabase Database Metadata Format

Metabase represents database metadata — synced databases, their tables, and their fields — as a tree of YAML files. Files are diff-friendly: numeric IDs are omitted entirely, and foreign keys use natural-key tuples like `["Sample Database", "PUBLIC", "ORDERS"]` instead of database identifiers.

This repository contains the specification, examples, and a CLI that converts the JSON returned by Metabase's `GET /api/database/metadata` endpoint into YAML.

## Contents

- **[core-spec/v1/spec.md](core-spec/v1/spec.md)** — Full specification (v1.0.0) covering entity keys, field types, folder structure, and each entity shape.
- **[examples/v1/](examples/v1/)** — Reference output using the Sample Database.
- **[src/](src/)** / **[bin/](bin/)** — The CLI implementation.

## Entities

| Entity | Description |
|--------|-------------|
| Database | A connected data source (Postgres, MySQL, BigQuery, etc.) |
| Table | A physical table (or view) inside a database |
| Field | A column on a table, including JSON-unfolded nested fields |

See [core-spec/v1/spec.md](core-spec/v1/spec.md) for the full schema of each entity.

## CLI

### Input: `metadata.json`

The CLI operates on a JSON snapshot produced by Metabase's `GET /api/database/metadata` endpoint. Fetch it against any running Metabase instance:

```sh
mkdir -p .metabase
curl "https://my.metabase/api/database/metadata" \
-H "X-Metabase-Session: $SESSION_TOKEN" \
> .metabase/metadata.json
```

The response is a flat structure with three arrays — `databases`, `tables`, and `fields` — streamed for large schemas. Authenticate with either a session token (`X-Metabase-Session`) or an API key (`X-API-Key`).

### Extract metadata to YAML

```sh
bunx @metabase/database-metadata extract-metadata <input-file> <output-folder>
```

- `<input-file>` — path to the `metadata.json` produced by the API.
- `<output-folder>` — destination directory. By convention this is `.metabase/databases` at the project root (see [spec.md](core-spec/v1/spec.md#folder-structure)). Database folders are created directly under it.

The typical end-to-end invocation:

```sh
bunx @metabase/database-metadata extract-metadata .metabase/metadata.json .metabase/databases
```

### Extract the spec

Copy the bundled `spec.md` to a target file:

```sh
bunx @metabase/database-metadata extract-spec --file ./spec.md
```

Omit `--file` to write `spec.md` into the current directory.

## Publishing to NPM

Releases are published automatically by the **Release to NPM** GitHub Actions workflow on every push to `main`. The workflow compares the `version` in `package.json` against the version published on npm and publishes (with the `latest` dist-tag) if they differ.

To cut a release, bump `version` in `package.json` and merge to `main`.

The workflow requires an `NPM_RELEASE_TOKEN` secret with publish access to the `@metabase` npm org.

## Development

```sh
bun install
bun bin/cli.js extract-metadata examples/v1/metadata.json /tmp/.metabase/databases
```

The **Validate** GitHub workflow regenerates the bundled examples on every push and fails if they drift from what's checked in.
58 changes: 58 additions & 0 deletions bin/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env node

import { parseArgs } from "node:util";
import { extractMetadata } from "../src/extract-metadata.js";
import { extractSpec } from "../src/extract-spec.js";

const { values, positionals } = parseArgs({
allowPositionals: true,
options: {
file: { type: "string" },
help: { type: "boolean", short: "h", default: false },
},
});

const command = positionals[0];

const HELP = `Usage: database-metadata <command> [arguments] [options]

Commands:
extract-metadata <input-file> <output-folder> Extract metadata JSON into YAML files
Writes one YAML per database + one per table
with fields nested inside.

extract-spec Copy the bundled spec.md into a target file
--file <path> Destination file (default: ./spec.md)

Options:
-h, --help Show this help message`;

if (values.help || !command) {
console.log(HELP);
process.exit(command ? 0 : 1);
}

if (command === "extract-metadata") {
const inputFile = positionals[1];
const outputFolder = positionals[2];

if (!inputFile || !outputFolder) {
console.error("Error: both <input-file> and <output-folder> arguments are required");
process.exit(1);
}

const stats = extractMetadata({ inputFile, outputFolder });
console.log(
`Extracted ${stats.databases} databases, ${stats.tables} tables, ${stats.fields} fields`,
);
process.exit(0);
}

if (command === "extract-spec") {
const { target } = extractSpec({ file: values.file ?? "spec.md" });
console.log(`Spec extracted to ${target}`);
process.exit(0);
}

console.error(`Unknown command: ${command}`);
process.exit(1);
17 changes: 17 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading