-
Notifications
You must be signed in to change notification settings - Fork 0
34 docs complete readme #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
190ae75
94da759
09faa4a
dcbe505
ab10ba3
cc3ee4b
a85eac4
a03738c
d1b89b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,138 @@ | ||
| # AL2DBML | ||
|
|
||
| Generates a DBML database schema from a Business Central AL project by parsing table and field definitions. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| No runtime required — AL2DBML is distributed as a self-contained binary. | ||
|
|
||
| | Platform | Supported | | ||
| |---|---| | ||
| | Windows (x64) | ✓ | | ||
| | macOS (Apple Silicon) | ✓ | | ||
| | Linux (x64) | ✓ | | ||
|
|
||
| ## Installation | ||
|
|
||
| ### Windows (PowerShell) | ||
|
|
||
| ```powershell | ||
| $dest = "$env:LOCALAPPDATA\al2dbml" | ||
| New-Item -ItemType Directory -Force -Path $dest | Out-Null | ||
| Invoke-WebRequest -Uri "https://github.com/OGR-67/AL2DBML/releases/latest/download/al2dbml-win-x64.exe" -OutFile "$dest\al2dbml.exe" | ||
| $path = [Environment]::GetEnvironmentVariable("Path", "User") | ||
| if ($path -notlike "*al2dbml*") { | ||
| [Environment]::SetEnvironmentVariable("Path", "$path;$dest", "User") | ||
| } | ||
| Write-Host "Done. Restart your terminal." | ||
| ``` | ||
|
|
||
| ### macOS | ||
|
|
||
| ```bash | ||
| curl -L https://github.com/OGR-67/AL2DBML/releases/latest/download/al2dbml-osx-arm64 -o al2dbml | ||
| chmod +x al2dbml | ||
| xattr -d com.apple.quarantine al2dbml | ||
| sudo mv al2dbml /usr/local/bin/al2dbml | ||
| ``` | ||
|
|
||
| > **Note:** The `xattr` command removes the Gatekeeper quarantine flag that macOS sets on downloaded binaries. Without it, macOS will block the app on first run. If you downloaded the binary manually instead of using this script, go to **System Settings → Privacy & Security** and click **Allow Anyway**. | ||
|
|
||
| ### Linux | ||
|
|
||
| ```bash | ||
| curl -L https://github.com/OGR-67/AL2DBML/releases/latest/download/al2dbml-linux-x64 -o al2dbml | ||
| chmod +x al2dbml | ||
| sudo mv al2dbml /usr/local/bin/al2dbml | ||
| ``` | ||
|
|
||
| ## Quick start | ||
|
|
||
| ```bash | ||
| # 1. Initialize AL2DBML in your project (run once) | ||
| al2dbml init | ||
|
|
||
| # 2. Generate the DBML schema | ||
| al2dbml generate | ||
| ``` | ||
|
|
||
| ## Commands | ||
|
|
||
| ### `generate` | ||
|
|
||
| Parses AL files and generates a `.dbml` schema file. | ||
|
|
||
| ``` | ||
| al2dbml generate [-i <input>] [-o <output>] [-n <name>] | ||
| ``` | ||
|
|
||
| | Option | Description | Default | | ||
| |---|---|---| | ||
| | `-i`, `--input` | Path to an AL project folder, `.al` file, or `.code-workspace` file | Value from `config.local.json`, then `.` | | ||
| | `-o`, `--output` | Output directory | Value from `config.json`, then `.` | | ||
| | `-n`, `--name` | Output file name (without extension) | Value from `config.json`, then `schema` | | ||
|
|
||
| ### `init` | ||
|
|
||
| Initializes AL2DBML interactively in the current directory. Creates the `.al2dbml/` config folder and optionally sets up a pre-commit hook. | ||
|
|
||
| ``` | ||
| al2dbml init | ||
| ``` | ||
|
|
||
| Re-running `init` on an existing config pre-fills the prompts with current values. | ||
|
|
||
| ## Configuration | ||
|
|
||
| `init` creates two files in `.al2dbml/`: | ||
|
|
||
| | File | Versioned | Content | | ||
| |---|---|---| | ||
| | `config.json` | Yes | Output path and file name — shared across contributors | | ||
| | `config.local.json` | No (gitignored) | Input path — contributor-specific, useful for workspaces | | ||
|
|
||
| `config.json` example: | ||
| ```json | ||
| { | ||
| "Output": { | ||
| "Path": "./docs/", | ||
| "Name": "schema" | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| `config.local.json` example: | ||
| ```json | ||
| { | ||
| "Input": { | ||
| "Path": "./MyProject.code-workspace" | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Pre-commit hook | ||
|
|
||
| When enabled during `init`, AL2DBML adds a section to `.git/hooks/pre-commit` that automatically regenerates the schema on each commit: | ||
|
|
||
| ```sh | ||
| # [al2dbml-start] | ||
| if command -v al2dbml > /dev/null 2>&1; then | ||
| al2dbml generate || printf "\033[33mWarning: al2dbml generate failed, skipping DBML update.\033[0m\n" | ||
| else | ||
| printf "\033[33mWarning: al2dbml not found, skipping DBML update.\033[0m\n" | ||
| fi | ||
| # [al2dbml-end] | ||
| ``` | ||
|
|
||
| If `al2dbml` is not installed on a contributor's machine, the hook prints a warning and lets the commit proceed normally. | ||
|
|
||
| To remove the hook, run: | ||
|
|
||
| ```bash | ||
| al2dbml remove-hook | ||
| ``` | ||
|
|
||
| ## Contributing | ||
|
|
||
| - [Architecture](docs/ARCHITECTURE.md) — project structure, CLI design, DI setup | ||
| - [AL Parsing](docs/AL_PARSING.md) — AL syntax specifics and parsing decisions | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| # AL File Parsing | ||
|
|
||
| This document describes the AL parsing specifics relevant to this project. It does not cover the full AL language specification — only what is needed to generate DBML output. | ||
|
|
||
| ## Supported file types | ||
|
|
||
| | AL type | Detection keyword | Handled by | | ||
| |---|---|---| | ||
| | Table | `table` | `ParseTable` | | ||
| | Table extension | `tableextension` | `ParseTableExtension` | | ||
| | Enum | `enum` | `ParseEnum` | | ||
| | Enum extension | `enumextension` | `ParseEnumExtension` | | ||
| | Other (page, report, codeunit...) | — | Silently skipped | | ||
|
|
||
| ## File type detection | ||
|
|
||
| Detection is done by regex on the **file content**, not the filename or extension. The detection order matters: | ||
|
|
||
| 1. `enumextension` before `enum` — an extension file could partially match the enum pattern | ||
| 2. `tableextension` before `table` — same reason | ||
|
|
||
| ## Object identifiers | ||
|
|
||
| Every AL object has a numeric ID: `table 50100 "Customer"`. The ID is **ignored** — only the name is extracted and used in the DBML output. | ||
|
|
||
| ## Name conventions | ||
|
|
||
| AL identifiers can be quoted or unquoted: | ||
| - `"My Field"` → quoted, typically used for names with spaces or special characters | ||
| - `MyField` → unquoted | ||
|
|
||
| Names containing `/` must be re-quoted in DBML output (e.g. `"No./Name"`) since `/` is not valid in an unquoted DBML identifier. This is handled by `AlSyntaxHelper.CleanName`. | ||
|
|
||
| ## Table parsing | ||
|
|
||
| ### Fields | ||
|
|
||
| AL field syntax: | ||
| ```al | ||
| field(50; "My Field"; Text[100]) { ... } | ||
| ``` | ||
|
|
||
| The field body `{ ... }` is parsed for: | ||
| - `FieldClass = FlowField` → marks the column as a FlowField | ||
| - `CalcFormula = ...` → extracts the formula as a string | ||
|
|
||
| ### Primary keys | ||
|
|
||
| Primary keys are detected from `key(...)` declarations: | ||
| - A key named `PK` is treated as the primary key | ||
| - A key with `Clustered = true` in its body is treated as the primary key | ||
| - Composite keys (comma-separated fields) are supported | ||
|
|
||
| ```al | ||
| key(PK; "No.") { Clustered = true; } | ||
| ``` | ||
|
|
||
| ## Table extensions | ||
|
|
||
| A `tableextension` extends an existing base table: | ||
| ```al | ||
| tableextension 50100 "My Extension" extends "Customer" { ... } | ||
| ``` | ||
|
|
||
| Fields from the extension are **merged** into the base table's `DBMLTable`. If the base table has not been parsed yet, a new `DBMLTable` is created for it and will be enriched when the base table file is processed. Parse order across files therefore does not matter. | ||
|
|
||
| ## Enum parsing | ||
|
|
||
| An enum can optionally implement one or more interfaces via the `implements` clause: | ||
|
|
||
| ```al | ||
| enum 50100 "My Status" implements "IStatus", "ILabel" | ||
| { | ||
| value(0; Open) { Caption = 'Open'; } | ||
| value(1; Closed) { Caption = 'Closed'; } | ||
| } | ||
| ``` | ||
|
|
||
| The `implements` clause is handled by the detection regex but the interface names are **not captured** in the output — only the enum name and its values matter for DBML. The corresponding interface definition files (`.al` files containing `interface "IStatus" { ... }`) are `Unknown` type and silently skipped. | ||
|
|
||
| ### Detection order caveat | ||
|
|
||
| The `Enum` detection pattern starts with `^\s*enum\s+`. Since `enumextension` also starts with `enum`, checking `Enum` before `EnumExtension` would misclassify extension files. The correct order is always `EnumExtension` → `Enum`. | ||
|
|
||
| ## Enum extensions | ||
|
|
||
| ```al | ||
| enumextension 50101 "My Status Ext" extends "My Status" { value(2; Pending) } | ||
| ``` | ||
|
|
||
| Values are merged into the base enum. Duplicate values are ignored. | ||
|
|
||
| ## FlowFields | ||
|
|
||
| AL FlowFields are calculated fields with no physical storage: | ||
| ```al | ||
| field(20; "Balance"; Decimal) { | ||
| FieldClass = FlowField; | ||
| CalcFormula = sum("Ledger Entry".Amount where("Account No." = field("No."))); | ||
| } | ||
| ``` | ||
|
|
||
| FlowFields are included in the DBML output as regular columns. The `CalcFormula` is captured and can be used as a note. DBML has no native equivalent for this concept. | ||
|
|
||
| ## State accumulation | ||
|
|
||
| `IAlParser` accumulates parsed tables and enums in internal state across multiple `Parse*` calls. This allows multi-file parsing (folder, workspace) to produce a single unified `OutputSchema`. State is cleared between command executions via `ClearOutputSchema()`. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,8 @@ | |||||
|
|
||||||
| AL2DBML follows a Clean Architecture pattern: the CLI resolves dependencies through a dedicated Composition Root, while the Parser acts as infrastructure implementing contracts defined by the Application layer — keeping the Domain model free of any external dependency. | ||||||
|
|
||||||
| ## Dependency graph | ||||||
|
|
||||||
| ```mermaid | ||||||
| graph TD | ||||||
| CLI["AL2DBML.CLI<br/><i>Presentation</i>"] | ||||||
|
|
@@ -24,3 +26,63 @@ graph TD | |||||
| TESTS --> CORE | ||||||
| TESTS --> DI | ||||||
| ``` | ||||||
|
|
||||||
| ## CLI layer | ||||||
|
|
||||||
| The CLI is built on **Spectre.Console.Cli** and exposes two commands: | ||||||
|
|
||||||
| | Command | Description | | ||||||
| |---|---| | ||||||
| | `generate` | Parses AL input and writes a `.dbml` file | | ||||||
| | `init` | Initializes the `.al2dbml/` config directory interactively | | ||||||
|
|
||||||
| ### Input strategy pattern | ||||||
|
|
||||||
| `generate` delegates input handling to an `IInputStrategy` resolved by `InputStrategyFactory` based on the detected input type: | ||||||
|
|
||||||
| ```mermaid | ||||||
| graph TD | ||||||
| GC["GenerateCommand"] | ||||||
| F["InputStrategyFactory"] | ||||||
| SF["SingleFileInputStrategy"] | ||||||
| FO["FolderInputStrategy"] | ||||||
| WS["WorkspaceInputStrategy"] | ||||||
|
|
||||||
| GC --> F | ||||||
| F --> SF | ||||||
| F --> FO | ||||||
| F --> WS | ||||||
| FO --> SF | ||||||
| WS --> FO | ||||||
| ``` | ||||||
|
|
||||||
| | Strategy | Input | Behaviour | | ||||||
| |---|---|---| | ||||||
| | `SingleFileInputStrategy` | `.al` file | Detects file type, calls the appropriate parser method | | ||||||
| | `FolderInputStrategy` | Directory | Scans for `*.al` recursively, delegates each file to `SingleFileInputStrategy` | | ||||||
| | `WorkspaceInputStrategy` | `.code-workspace` | Reads workspace JSON, resolves each folder path, delegates to `FolderInputStrategy` | | ||||||
|
|
||||||
| Files with an unsupported type (`AlFileType.Unknown`) are silently skipped — this is intentional since AL projects contain many non-table/enum objects. | ||||||
|
|
||||||
| ### Configuration | ||||||
|
|
||||||
| `init` creates a `.al2dbml/` directory at the project root with two files: | ||||||
|
|
||||||
| | File | Tracked | Content | | ||||||
| |---|---|---| | ||||||
| | `config.json` | versioned | output path and file name (shared across contributors) | | ||||||
| | `config.local.json` | gitignored | input path (contributor-specific, especially for workspaces) | | ||||||
|
|
||||||
| `generate` reads these files if present; CLI arguments always take precedence. | ||||||
|
|
||||||
| ### Services | ||||||
|
|
||||||
| | Service | Scope | Responsibility | | ||||||
| |---|---|---| | ||||||
| | `FileSystemService` | static | Input type detection, directory scan | | ||||||
| | `ConfigService` | scoped | Read/write `.al2dbml/` config files | | ||||||
| | `ParsingTracker` | scoped | Track file count and elapsed time per command execution | | ||||||
|
|
||||||
| ### DI integration | ||||||
|
|
||||||
| Spectre.Console.Cli is integrated with `Microsoft.Extensions.DependencyInjection` via a custom `TypeRegistrar`/`TypeResolver`. A new DI scope is created per command execution so scoped services (parser state, tracker) are properly isolated. | ||||||
|
||||||
| Spectre.Console.Cli is integrated with `Microsoft.Extensions.DependencyInjection` via a custom `TypeRegistrar`/`TypeResolver`. A new DI scope is created per command execution so scoped services (parser state, tracker) are properly isolated. | |
| Spectre.Console.Cli is integrated with `Microsoft.Extensions.DependencyInjection` via a custom `TypeRegistrar`/`TypeResolver`. The DI container is built once and a single scope is reused for the application lifetime, so services registered as scoped (such as parser state and trackers) are shared across command executions rather than isolated per command. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
al2dbml remove-hookis documented here, but the CLI currently only registersgenerateandinitcommands (noremove-hook). Either add aremove-hookcommand, or update the README to describe manual removal (delete the section between# [al2dbml-start]/# [al2dbml-end]in.git/hooks/pre-commit).