dbtour 0.2.0 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/dbtour.mjs CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- // Node 22.18+ runs the TypeScript as it is; an older Node fails here with a syntax error, so say why first.
2
+ // Node 22.18+ is what the published JavaScript targets; an older Node fails on syntax, so say why first.
3
3
  const [major, minor] = process.versions.node.split('.').map(Number);
4
4
  if (major < 22 || (major === 22 && minor < 18)) {
5
5
  console.error(`dbtour needs Node 22.18 or newer (this is ${process.versions.node}). Upgrade Node, then run it again.`);
6
6
  process.exit(2);
7
7
  }
8
- const { main } = await import('../src/cli.ts');
8
+ // dist/ is what `npm run build` emits and what the package ships; in the repository, build first.
9
+ const { main } = await import('../dist/cli.js');
9
10
  process.exitCode = await main(process.argv.slice(2));
package/dist/cli.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { type AddonConfig, type SourceConfig } from './config.ts';
2
+ import { type Addon, type Source } from './plugins.ts';
3
+ export interface Io {
4
+ cwd: string;
5
+ log: (line: string) => void;
6
+ error: (line: string) => void;
7
+ env?: Record<string, string | undefined>;
8
+ /** How a source is loaded; tests hand in their own. */
9
+ load?: (source: SourceConfig) => Promise<Source>;
10
+ /** How an add-on is loaded; tests hand in their own. */
11
+ loadAddon?: (addon: AddonConfig) => Promise<Addon>;
12
+ }
13
+ export declare function main(argv: string[], io?: Io): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,156 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ // The command. Reads dbtour.yaml in the working directory, writes under its `out`. Exit 1 for what was
10
+ // asked and failed, 2 for what is missing, 0 otherwise.
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
12
+ import { dirname, join, resolve } from 'node:path';
13
+ import { pathToFileURL } from 'node:url';
14
+ import { parseArgs } from 'node:util';
15
+ import { configFile, defaultConfigText, modelFile, parseConfig, resolveConnection } from "./config.js";
16
+ import { emptyModel, validate } from "./model.js";
17
+ import { applyNotes, parseNotes } from "./notes.js";
18
+ import { loadAddon, loadSource } from "./plugins.js";
19
+ import { renderMarkdown } from "./render/markdown.js";
20
+ const help = `Usage: dbtour <command>
21
+
22
+ init [--source mssql] write ${configFile} here
23
+ extract read every source in ${configFile}, write <out>/${modelFile}
24
+ render read <out>/${modelFile} and the notes, write the Markdown and what the
25
+ add-ons in ${configFile} render (explorer: index.html) under <out>
26
+
27
+ Set the connection string in the environment variable ${configFile} names, never in the file.`;
28
+ export async function main(argv, io = { cwd: process.cwd(), log: console.log, error: console.error }) {
29
+ const { values, positionals } = parseArgs({ args: argv, options: { help: { type: 'boolean', short: 'h' }, source: { type: 'string' } }, allowPositionals: true, strict: false });
30
+ const command = positionals[0];
31
+ if (values.help || command === undefined) {
32
+ io.log(help);
33
+ return 0;
34
+ }
35
+ try {
36
+ switch (command) {
37
+ case 'init':
38
+ return init(io, typeof values.source === 'string' ? values.source : 'mssql');
39
+ case 'extract':
40
+ return await extract(io);
41
+ case 'render':
42
+ return await render(io);
43
+ default:
44
+ io.error(`"${command}" is not a dbtour command.\n\n${help}`);
45
+ return 1;
46
+ }
47
+ }
48
+ catch (error) {
49
+ io.error(error instanceof Error ? error.message : String(error));
50
+ return error.exitCode ?? 1;
51
+ }
52
+ }
53
+ function missing(message) {
54
+ return Object.assign(new Error(message), { exitCode: 2 });
55
+ }
56
+ function init(io, source) {
57
+ const path = join(io.cwd, configFile);
58
+ if (existsSync(path)) {
59
+ io.error(`${configFile} already exists here. Edit it, or delete it and run dbtour init again.`);
60
+ return 1;
61
+ }
62
+ writeFileSync(path, defaultConfigText(source));
63
+ io.log(`Wrote ${configFile}. Set the connection string in DBTOUR_${source.toUpperCase()}, then run dbtour extract.`);
64
+ return 0;
65
+ }
66
+ function readConfig(io) {
67
+ const path = join(io.cwd, configFile);
68
+ if (!existsSync(path))
69
+ throw missing(`No ${configFile} here. Run dbtour init to write one.`);
70
+ return parseConfig(readFileSync(path, 'utf8'));
71
+ }
72
+ async function extract(io) {
73
+ const config = readConfig(io);
74
+ const env = io.env ?? process.env;
75
+ const load = io.load ?? ((source) => loadSource(source, importer(io.cwd)));
76
+ const model = { ...emptyModel(), generatedAt: new Date().toISOString() };
77
+ for (const source of config.sources) {
78
+ const connection = resolveConnection(source.connection, env);
79
+ const { extract } = await load(source);
80
+ let result;
81
+ try {
82
+ result = await extract({ ...source, connection });
83
+ }
84
+ catch (error) {
85
+ io.error(`${source.name} (${source.type}): ${error instanceof Error ? error.message : String(error)}`);
86
+ return 1;
87
+ }
88
+ if ('schemas' in result) {
89
+ model.databases.push(result);
90
+ const tables = result.schemas.reduce((n, s) => n + s.tables.length, 0);
91
+ io.log(`${result.name}: ${tables} tables, ${result.relations.length} relations`);
92
+ }
93
+ else {
94
+ model.apis.push(result);
95
+ io.log(`${result.name}: ${result.endpoints.length} endpoints`);
96
+ }
97
+ }
98
+ const checked = validate(model);
99
+ if (!checked.ok) {
100
+ io.error(`A source returned something that is not a model:\n ${checked.errors.join('\n ')}`);
101
+ return 1;
102
+ }
103
+ const path = join(io.cwd, config.out, modelFile);
104
+ mkdirSync(dirname(path), { recursive: true });
105
+ writeFileSync(path, JSON.stringify(model, null, 2) + '\n');
106
+ io.log(`Wrote ${join(config.out, modelFile)}. Run dbtour render to write the docs.`);
107
+ return 0;
108
+ }
109
+ /** A module given as a path is relative to the project, not to dbtour. */
110
+ function importer(cwd) {
111
+ return (specifier) => import(__rewriteRelativeImportExtension(specifier.startsWith('.') || specifier.startsWith('/') ? pathToFileURL(resolve(cwd, specifier)).href : specifier));
112
+ }
113
+ async function render(io) {
114
+ const config = readConfig(io);
115
+ const modelPath = join(io.cwd, config.out, modelFile);
116
+ if (!existsSync(modelPath))
117
+ throw missing(`No ${join(config.out, modelFile)} yet. Run dbtour extract to write it.`);
118
+ const checked = validate(JSON.parse(readFileSync(modelPath, 'utf8')));
119
+ if (!checked.ok) {
120
+ io.error(`${join(config.out, modelFile)} is not a model:\n ${checked.errors.join('\n ')}`);
121
+ return 1;
122
+ }
123
+ let model = checked.model;
124
+ const notesPath = join(io.cwd, config.notes);
125
+ if (existsSync(notesPath)) {
126
+ const applied = applyNotes(model, parseNotes(readFileSync(notesPath, 'utf8')));
127
+ for (const warning of applied.warnings)
128
+ io.error(warning);
129
+ model = applied.model;
130
+ }
131
+ // Every add-on is loaded before anything is written, so a missing one stops the run with nothing half done.
132
+ const load = io.loadAddon ?? ((addon) => loadAddon(addon, importer(io.cwd)));
133
+ const addons = [];
134
+ for (const addon of config.addons) {
135
+ try {
136
+ addons.push(await load(addon));
137
+ }
138
+ catch (error) {
139
+ throw missing(error instanceof Error ? error.message : String(error));
140
+ }
141
+ }
142
+ for (const addon of addons)
143
+ if (addon.enrich)
144
+ model = await addon.enrich(model);
145
+ const files = renderMarkdown(model);
146
+ for (const addon of addons)
147
+ if (addon.render)
148
+ Object.assign(files, await addon.render(model));
149
+ for (const [name, text] of Object.entries(files)) {
150
+ const path = join(io.cwd, config.out, name);
151
+ mkdirSync(dirname(path), { recursive: true });
152
+ writeFileSync(path, text);
153
+ }
154
+ io.log(`Wrote ${Object.keys(files).length} files to ${config.out}.`);
155
+ return 0;
156
+ }
@@ -0,0 +1,26 @@
1
+ export interface SourceConfig {
2
+ type: string;
3
+ name: string;
4
+ /** `${NAME}`: the environment variable holding the connection string. Never the string itself. */
5
+ connection?: string;
6
+ /** A module to import instead of dbtour-source-<type>. */
7
+ module?: string;
8
+ [option: string]: unknown;
9
+ }
10
+ export interface AddonConfig {
11
+ type: string;
12
+ /** A module to import instead of dbtour-addon-<type>. */
13
+ module?: string;
14
+ [option: string]: unknown;
15
+ }
16
+ export interface Config {
17
+ out: string;
18
+ notes: string;
19
+ sources: SourceConfig[];
20
+ addons: AddonConfig[];
21
+ }
22
+ export declare const configFile = "dbtour.yaml";
23
+ export declare const modelFile = "schema.json";
24
+ export declare function parseConfig(text: string): Config;
25
+ export declare function defaultConfigText(type: string): string;
26
+ export declare function resolveConnection(value: string | undefined, env: Record<string, string | undefined>): string | undefined;
package/dist/config.js ADDED
@@ -0,0 +1,75 @@
1
+ // dbtour.yaml: where the model goes, where it comes from, where the notes are.
2
+ import { parse } from 'yaml';
3
+ export const configFile = 'dbtour.yaml';
4
+ export const modelFile = 'schema.json';
5
+ export function parseConfig(text) {
6
+ const raw = parse(text) ?? {};
7
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
8
+ throw new Error(`${configFile} must be a map with out, notes and sources`);
9
+ const doc = raw;
10
+ if (!Array.isArray(doc.sources) || doc.sources.length === 0)
11
+ throw new Error(`${configFile} needs a sources list with at least one source`);
12
+ const sources = doc.sources.map((value, i) => {
13
+ if (typeof value !== 'object' || value === null)
14
+ throw new Error(`sources[${i}] must be a map with type and name`);
15
+ const entry = value;
16
+ if (typeof entry.type !== 'string' || entry.type === '')
17
+ throw new Error(`sources[${i}].type is required: mssql, for instance`);
18
+ if (typeof entry.name !== 'string' || entry.name === '')
19
+ throw new Error(`sources[${i}].name is required: the name the docs call this database`);
20
+ return { ...entry, type: entry.type, name: entry.name };
21
+ });
22
+ return {
23
+ out: typeof doc.out === 'string' ? doc.out : 'docs/dbtour',
24
+ notes: typeof doc.notes === 'string' ? doc.notes : 'dbtour.notes.yaml',
25
+ sources,
26
+ addons: parseAddons(doc.addons),
27
+ };
28
+ }
29
+ /** `addons: [explorer]` or `addons: [{ type: explorer, module: ./page.ts }]`; none when absent. */
30
+ function parseAddons(value) {
31
+ if (value === undefined)
32
+ return [];
33
+ if (!Array.isArray(value))
34
+ throw new Error('addons must be a list of add-on names, like [explorer]');
35
+ return value.map((entry, i) => {
36
+ if (typeof entry === 'string' && entry !== '')
37
+ return { type: entry };
38
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry))
39
+ throw new Error(`addons[${i}] must be a name or a map with type`);
40
+ const map = entry;
41
+ if (typeof map.type !== 'string' || map.type === '')
42
+ throw new Error(`addons[${i}].type is required: explorer, for instance`);
43
+ return { ...map, type: map.type };
44
+ });
45
+ }
46
+ export function defaultConfigText(type) {
47
+ const variable = `DBTOUR_${type.toUpperCase()}`;
48
+ return `# dbtour: see https://github.com/victorsaly/dbtour
49
+ out: docs/dbtour
50
+ sources:
51
+ - type: ${type}
52
+ name: ${type === 'mssql' ? 'MyDatabase' : 'my-database'}
53
+ # The name of the environment variable holding the connection string. Never the string itself.
54
+ connection: \${${variable}}
55
+ # Descriptions, relations and groups the database cannot say. Optional.
56
+ notes: dbtour.notes.yaml
57
+ # What else render writes. explorer is the page: npm install --save-dev dbtour-addon-explorer
58
+ addons:
59
+ - explorer
60
+ `;
61
+ }
62
+ const variablePattern = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/;
63
+ export function resolveConnection(value, env) {
64
+ if (value === undefined)
65
+ return undefined;
66
+ const match = variablePattern.exec(value);
67
+ if (!match)
68
+ throw new Error(`connection must name an environment variable, like \${DBTOUR_MSSQL}: never write the connection string into ${configFile}`);
69
+ const resolved = env[match[1]];
70
+ if (resolved === undefined || resolved === '') {
71
+ // Exit 2: what is missing, not what failed.
72
+ throw Object.assign(new Error(`Set ${match[1]} to the connection string, then run it again`), { exitCode: 2 });
73
+ }
74
+ return resolved;
75
+ }
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./model.js";
2
+ export { parseNotes, applyNotes } from "./notes.js";
3
+ export { parseConfig, defaultConfigText, resolveConnection, configFile, modelFile } from "./config.js";
4
+ export { loadSource, sourceModuleName, loadAddon, addonModuleName } from "./plugins.js";
5
+ export { renderMarkdown, renderTableMarkdown, driverName, wholeDiagramLimit } from "./render/markdown.js";
6
+ export { erDiagram, neighbourhood, entityName } from "./render/mermaid.js";
@@ -0,0 +1,121 @@
1
+ export declare const MODEL_VERSION = 1;
2
+ export interface Column {
3
+ name: string;
4
+ /** As the database spells it, with its size: nvarchar(320), decimal(18,2). */
5
+ type: string;
6
+ nullable: boolean;
7
+ default?: string;
8
+ /** The expression of a computed column. */
9
+ computed?: string;
10
+ identity?: boolean;
11
+ description?: string;
12
+ }
13
+ export interface Index {
14
+ name: string;
15
+ columns: string[];
16
+ unique: boolean;
17
+ included?: string[];
18
+ }
19
+ export interface Constraint {
20
+ name: string;
21
+ kind: 'primary' | 'unique' | 'check' | 'foreign';
22
+ columns: string[];
23
+ definition?: string;
24
+ }
25
+ export interface Table {
26
+ name: string;
27
+ kind: 'table' | 'view';
28
+ description?: string;
29
+ columns: Column[];
30
+ primaryKey?: string[];
31
+ indexes: Index[];
32
+ constraints: Constraint[];
33
+ /** The SQL of a view. */
34
+ definition?: string;
35
+ }
36
+ export interface Schema {
37
+ name: string;
38
+ tables: Table[];
39
+ }
40
+ export interface RelationEnd {
41
+ table: string;
42
+ columns: string[];
43
+ }
44
+ export interface Relation {
45
+ name?: string;
46
+ from: RelationEnd;
47
+ to: RelationEnd;
48
+ /** Declared in the notes, not in the database. */
49
+ virtual?: boolean;
50
+ onDelete?: string;
51
+ onUpdate?: string;
52
+ }
53
+ export interface Database {
54
+ name: string;
55
+ driver: string;
56
+ version?: string;
57
+ description?: string;
58
+ schemas: Schema[];
59
+ relations: Relation[];
60
+ }
61
+ export interface Endpoint {
62
+ method: string;
63
+ path: string;
64
+ summary?: string;
65
+ description?: string;
66
+ tags?: string[];
67
+ }
68
+ export interface Api {
69
+ name: string;
70
+ description?: string;
71
+ endpoints: Endpoint[];
72
+ }
73
+ /** An endpoint and the tables it touches. */
74
+ export interface Link {
75
+ api: string;
76
+ method: string;
77
+ path: string;
78
+ tables: string[];
79
+ }
80
+ export interface Group {
81
+ name: string;
82
+ description?: string;
83
+ tables: string[];
84
+ }
85
+ export interface Model {
86
+ dbtour: typeof MODEL_VERSION;
87
+ generatedAt?: string;
88
+ databases: Database[];
89
+ apis: Api[];
90
+ links: Link[];
91
+ groups: Group[];
92
+ }
93
+ export declare function emptyModel(): Model;
94
+ export interface TableRef {
95
+ schema: string;
96
+ table: string;
97
+ column?: string;
98
+ }
99
+ export declare function tableRef(schema: string, table: string): string;
100
+ export declare function parseRef(ref: string): TableRef;
101
+ export interface Found {
102
+ database: Database;
103
+ schema: Schema;
104
+ table: Table;
105
+ }
106
+ export declare function findTable(model: Model, ref: string): Found | undefined;
107
+ /** Unique on its own, by a constraint or an index, and not the primary key, which says so already. */
108
+ export declare function isUniqueColumn(table: Table, column: string): boolean;
109
+ export declare function relationsOf(model: Model, ref: string): {
110
+ out: Relation[];
111
+ in: Relation[];
112
+ };
113
+ export type Validation = {
114
+ ok: true;
115
+ model: Model;
116
+ } | {
117
+ ok: false;
118
+ errors: string[];
119
+ };
120
+ /** Checks the shape, then that every relation points at a table in its database. */
121
+ export declare function validate(value: unknown): Validation;
package/dist/model.js ADDED
@@ -0,0 +1,210 @@
1
+ // The model: what every source writes and everything else reads. Kept to plain JSON so it can be committed,
2
+ // diffed and handed to a page or an agent. Refs name tables as `schema.table` and columns as
3
+ // `schema.table.column`.
4
+ export const MODEL_VERSION = 1;
5
+ export function emptyModel() {
6
+ return { dbtour: MODEL_VERSION, databases: [], apis: [], links: [], groups: [] };
7
+ }
8
+ export function tableRef(schema, table) {
9
+ return `${schema}.${table}`;
10
+ }
11
+ export function parseRef(ref) {
12
+ const parts = ref.split('.');
13
+ if (parts.length < 2 || parts.length > 3 || parts.some((p) => p === '')) {
14
+ throw new Error(`"${ref}" is not a ref: write schema.table, or schema.table.column`);
15
+ }
16
+ const [schema, table, column] = parts;
17
+ return column === undefined ? { schema, table } : { schema, table, column };
18
+ }
19
+ export function findTable(model, ref) {
20
+ const { schema: schemaName, table: tableName } = parseRef(ref);
21
+ for (const database of model.databases) {
22
+ for (const schema of database.schemas) {
23
+ if (schema.name !== schemaName)
24
+ continue;
25
+ const table = schema.tables.find((t) => t.name === tableName);
26
+ if (table)
27
+ return { database, schema, table };
28
+ }
29
+ }
30
+ return undefined;
31
+ }
32
+ /** Unique on its own, by a constraint or an index, and not the primary key, which says so already. */
33
+ export function isUniqueColumn(table, column) {
34
+ if (table.primaryKey?.includes(column))
35
+ return false;
36
+ return (table.constraints.some((c) => c.kind === 'unique' && c.columns.length === 1 && c.columns[0] === column) ||
37
+ table.indexes.some((i) => i.unique && i.columns.length === 1 && i.columns[0] === column));
38
+ }
39
+ export function relationsOf(model, ref) {
40
+ const out = [];
41
+ const inward = [];
42
+ for (const database of model.databases) {
43
+ for (const relation of database.relations) {
44
+ if (relation.from.table === ref)
45
+ out.push(relation);
46
+ if (relation.to.table === ref)
47
+ inward.push(relation);
48
+ }
49
+ }
50
+ return { out, in: inward };
51
+ }
52
+ /** Checks the shape, then that every relation points at a table in its database. */
53
+ export function validate(value) {
54
+ const errors = [];
55
+ const check = new Checker(errors);
56
+ if (!check.object(value, 'model'))
57
+ return { ok: false, errors };
58
+ const model = value;
59
+ if (model.dbtour !== MODEL_VERSION)
60
+ errors.push(`model.dbtour must be ${MODEL_VERSION}, got ${JSON.stringify(model.dbtour)}`);
61
+ check.optionalString(model.generatedAt, 'generatedAt');
62
+ check.list(model.databases, 'databases', (d, p) => checkDatabase(check, d, p));
63
+ check.list(model.apis, 'apis', (a, p) => {
64
+ if (!check.object(a, p))
65
+ return;
66
+ check.string(a.name, `${p}.name`);
67
+ check.optionalString(a.description, `${p}.description`);
68
+ check.list(a.endpoints, `${p}.endpoints`, (e, ep) => {
69
+ if (!check.object(e, ep))
70
+ return;
71
+ check.string(e.method, `${ep}.method`);
72
+ check.string(e.path, `${ep}.path`);
73
+ check.optionalString(e.summary, `${ep}.summary`);
74
+ check.optionalString(e.description, `${ep}.description`);
75
+ if (e.tags !== undefined)
76
+ check.strings(e.tags, `${ep}.tags`);
77
+ });
78
+ });
79
+ check.list(model.links, 'links', (l, p) => {
80
+ if (!check.object(l, p))
81
+ return;
82
+ check.string(l.api, `${p}.api`);
83
+ check.string(l.method, `${p}.method`);
84
+ check.string(l.path, `${p}.path`);
85
+ check.strings(l.tables, `${p}.tables`);
86
+ });
87
+ check.list(model.groups, 'groups', (g, p) => {
88
+ if (!check.object(g, p))
89
+ return;
90
+ check.string(g.name, `${p}.name`);
91
+ check.optionalString(g.description, `${p}.description`);
92
+ check.strings(g.tables, `${p}.tables`);
93
+ });
94
+ if (errors.length > 0)
95
+ return { ok: false, errors };
96
+ const typed = value;
97
+ typed.databases.forEach((database, i) => {
98
+ const refs = new Set(database.schemas.flatMap((s) => s.tables.map((t) => tableRef(s.name, t.name))));
99
+ database.relations.forEach((relation, j) => {
100
+ for (const end of ['from', 'to']) {
101
+ if (!refs.has(relation[end].table)) {
102
+ errors.push(`databases[${i}].relations[${j}].${end}.table "${relation[end].table}" is not a table of ${database.name}`);
103
+ }
104
+ }
105
+ });
106
+ });
107
+ return errors.length > 0 ? { ok: false, errors } : { ok: true, model: typed };
108
+ }
109
+ function checkDatabase(check, d, p) {
110
+ if (!check.object(d, p))
111
+ return;
112
+ check.string(d.name, `${p}.name`);
113
+ check.string(d.driver, `${p}.driver`);
114
+ check.optionalString(d.version, `${p}.version`);
115
+ check.optionalString(d.description, `${p}.description`);
116
+ check.list(d.schemas, `${p}.schemas`, (s, sp) => {
117
+ if (!check.object(s, sp))
118
+ return;
119
+ check.string(s.name, `${sp}.name`);
120
+ check.list(s.tables, `${sp}.tables`, (t, tp) => checkTable(check, t, tp));
121
+ });
122
+ check.list(d.relations, `${p}.relations`, (r, rp) => {
123
+ if (!check.object(r, rp))
124
+ return;
125
+ check.optionalString(r.name, `${rp}.name`);
126
+ for (const end of ['from', 'to']) {
127
+ const e = r[end];
128
+ if (!check.object(e, `${rp}.${end}`))
129
+ continue;
130
+ check.string(e.table, `${rp}.${end}.table`);
131
+ check.strings(e.columns, `${rp}.${end}.columns`);
132
+ }
133
+ check.optionalBoolean(r.virtual, `${rp}.virtual`);
134
+ });
135
+ }
136
+ function checkTable(check, t, p) {
137
+ if (!check.object(t, p))
138
+ return;
139
+ check.string(t.name, `${p}.name`);
140
+ if (t.kind !== 'table' && t.kind !== 'view')
141
+ check.errors.push(`${p}.kind must be "table" or "view"`);
142
+ check.optionalString(t.description, `${p}.description`);
143
+ check.optionalString(t.definition, `${p}.definition`);
144
+ if (t.primaryKey !== undefined)
145
+ check.strings(t.primaryKey, `${p}.primaryKey`);
146
+ check.list(t.columns, `${p}.columns`, (c, cp) => {
147
+ if (!check.object(c, cp))
148
+ return;
149
+ check.string(c.name, `${cp}.name`);
150
+ check.string(c.type, `${cp}.type`);
151
+ if (typeof c.nullable !== 'boolean')
152
+ check.errors.push(`${cp}.nullable must be true or false`);
153
+ check.optionalString(c.default, `${cp}.default`);
154
+ check.optionalString(c.computed, `${cp}.computed`);
155
+ check.optionalBoolean(c.identity, `${cp}.identity`);
156
+ check.optionalString(c.description, `${cp}.description`);
157
+ });
158
+ check.list(t.indexes, `${p}.indexes`, (x, xp) => {
159
+ if (!check.object(x, xp))
160
+ return;
161
+ check.string(x.name, `${xp}.name`);
162
+ check.strings(x.columns, `${xp}.columns`);
163
+ if (typeof x.unique !== 'boolean')
164
+ check.errors.push(`${xp}.unique must be true or false`);
165
+ });
166
+ check.list(t.constraints, `${p}.constraints`, (c, cp) => {
167
+ if (!check.object(c, cp))
168
+ return;
169
+ check.string(c.name, `${cp}.name`);
170
+ if (!['primary', 'unique', 'check', 'foreign'].includes(String(c.kind)))
171
+ check.errors.push(`${cp}.kind is not a constraint kind`);
172
+ check.strings(c.columns, `${cp}.columns`);
173
+ check.optionalString(c.definition, `${cp}.definition`);
174
+ });
175
+ }
176
+ class Checker {
177
+ errors;
178
+ constructor(errors) {
179
+ this.errors = errors;
180
+ }
181
+ object(value, path) {
182
+ if (typeof value === 'object' && value !== null && !Array.isArray(value))
183
+ return true;
184
+ this.errors.push(`${path} must be an object`);
185
+ return false;
186
+ }
187
+ string(value, path) {
188
+ if (typeof value !== 'string' || value === '')
189
+ this.errors.push(`${path} must be a non-empty string`);
190
+ }
191
+ optionalString(value, path) {
192
+ if (value !== undefined && typeof value !== 'string')
193
+ this.errors.push(`${path} must be a string when given`);
194
+ }
195
+ optionalBoolean(value, path) {
196
+ if (value !== undefined && typeof value !== 'boolean')
197
+ this.errors.push(`${path} must be true or false when given`);
198
+ }
199
+ strings(value, path) {
200
+ if (!Array.isArray(value) || value.some((v) => typeof v !== 'string'))
201
+ this.errors.push(`${path} must be a list of strings`);
202
+ }
203
+ list(value, path, each) {
204
+ if (!Array.isArray(value)) {
205
+ this.errors.push(`${path} must be a list`);
206
+ return;
207
+ }
208
+ value.forEach((item, i) => each(item, `${path}[${i}]`));
209
+ }
210
+ }
@@ -0,0 +1,21 @@
1
+ import { type Group, type Model } from './model.ts';
2
+ export interface TableNotes {
3
+ description?: string;
4
+ columns?: Record<string, string>;
5
+ }
6
+ export interface RelationNote {
7
+ from: string;
8
+ to: string;
9
+ }
10
+ export interface Notes {
11
+ tables: Record<string, TableNotes>;
12
+ relations: RelationNote[];
13
+ groups: Group[];
14
+ }
15
+ export declare function parseNotes(text: string): Notes;
16
+ export interface Applied {
17
+ model: Model;
18
+ /** What the notes name that the model does not have; said, then skipped. */
19
+ warnings: string[];
20
+ }
21
+ export declare function applyNotes(input: Model, notes: Notes): Applied;