dbtour 0.3.0 → 0.4.0

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/dist/cli.js CHANGED
@@ -89,6 +89,9 @@ async function extract(io) {
89
89
  model.databases.push(result);
90
90
  const tables = result.schemas.reduce((n, s) => n + s.tables.length, 0);
91
91
  io.log(`${result.name}: ${tables} tables, ${result.relations.length} relations`);
92
+ // Not a failure: the rest was read, and an absence the model records is not taken for a no.
93
+ for (const reason of result.unread ?? [])
94
+ io.log(`${result.name}: not read: ${reason}. Grant it to read them, or leave them out.`);
92
95
  }
93
96
  else {
94
97
  model.apis.push(result);
package/dist/model.d.ts CHANGED
@@ -1,4 +1,15 @@
1
1
  export declare const MODEL_VERSION = 1;
2
+ /**
3
+ * Extended properties other than MS_Description (which is `description`), by name: what a team records about
4
+ * an object in the database itself, such as its owner, source or retention.
5
+ */
6
+ export type Properties = Record<string, string>;
7
+ /** A column's sensitivity classification, as SQL Server's data discovery or a team set it. */
8
+ export interface Sensitivity {
9
+ label?: string;
10
+ informationType?: string;
11
+ rank?: string;
12
+ }
2
13
  export interface Column {
3
14
  name: string;
4
15
  /** As the database spells it, with its size: nvarchar(320), decimal(18,2). */
@@ -8,19 +19,62 @@ export interface Column {
8
19
  /** The expression of a computed column. */
9
20
  computed?: string;
10
21
  identity?: boolean;
22
+ /** An identity's seed and increment, as the database spells them. */
23
+ identitySeed?: string;
24
+ identityIncrement?: string;
11
25
  description?: string;
26
+ /** Only where it differs from the database's collation. */
27
+ collation?: string;
28
+ sparse?: boolean;
29
+ /** The dynamic data masking function: default(), email(), partial(2,"XX",0). */
30
+ masked?: string;
31
+ /** Always Encrypted: how, and under which column encryption key. */
32
+ encrypted?: {
33
+ type: 'deterministic' | 'randomized';
34
+ key: string;
35
+ };
36
+ sensitivity?: Sensitivity;
37
+ properties?: Properties;
12
38
  }
13
39
  export interface Index {
14
40
  name: string;
15
41
  columns: string[];
16
42
  unique: boolean;
17
43
  included?: string[];
44
+ /** As the database names the kind: nonclustered, clustered columnstore, xml, spatial. */
45
+ type?: string;
46
+ /** A filtered index's WHERE. */
47
+ filter?: string;
48
+ disabled?: boolean;
49
+ properties?: Properties;
18
50
  }
19
51
  export interface Constraint {
20
52
  name: string;
21
53
  kind: 'primary' | 'unique' | 'check' | 'foreign';
22
54
  columns: string[];
23
55
  definition?: string;
56
+ /** A foreign key or check the database no longer enforces. */
57
+ disabled?: boolean;
58
+ /** Enforced, but not checked against the rows already there, so the optimiser cannot rely on it. */
59
+ untrusted?: boolean;
60
+ properties?: Properties;
61
+ }
62
+ export interface Trigger {
63
+ name: string;
64
+ timing: 'after' | 'instead of';
65
+ /** insert, update, delete. */
66
+ events: string[];
67
+ enabled: boolean;
68
+ definition?: string;
69
+ }
70
+ /** A row-level security predicate on the table: a filter hides rows, a block refuses writes. */
71
+ export interface SecurityPredicate {
72
+ policy: string;
73
+ kind: 'filter' | 'block';
74
+ /** For a block predicate: after insert, before update, and so on. */
75
+ operation?: string;
76
+ predicate: string;
77
+ enabled: boolean;
24
78
  }
25
79
  /**
26
80
  * How big a table is, as the database keeps it about itself: counted from its catalogue, never by reading
@@ -44,10 +98,37 @@ export interface Table {
44
98
  /** The SQL of a view. */
45
99
  definition?: string;
46
100
  stats?: TableStats;
101
+ properties?: Properties;
102
+ /** When the table was created and its definition last altered, as ISO timestamps. */
103
+ created?: string;
104
+ altered?: string;
105
+ /** A system-versioned table: its history table (a ref) and the columns that bound each row's period. */
106
+ temporal?: {
107
+ history: string;
108
+ start: string;
109
+ end: string;
110
+ };
111
+ /** The history table of a system-versioned table, as its ref. */
112
+ historyOf?: string;
113
+ memoryOptimised?: 'schema and data' | 'schema only';
114
+ ledger?: 'updatable' | 'append-only' | 'history';
115
+ partitioning?: {
116
+ scheme: string;
117
+ function: string;
118
+ column: string;
119
+ partitions: number;
120
+ };
121
+ /** The table's own compression, when it has any: row, page, columnstore, columnstore archive. */
122
+ compression?: string;
123
+ changeTracking?: boolean;
124
+ cdc?: boolean;
125
+ triggers?: Trigger[];
126
+ security?: SecurityPredicate[];
47
127
  }
48
128
  export interface Schema {
49
129
  name: string;
50
130
  tables: Table[];
131
+ properties?: Properties;
51
132
  }
52
133
  export interface RelationEnd {
53
134
  table: string;
@@ -61,6 +142,9 @@ export interface Relation {
61
142
  virtual?: boolean;
62
143
  onDelete?: string;
63
144
  onUpdate?: string;
145
+ /** As on its constraint: not enforced, or not checked against the rows already there. */
146
+ disabled?: boolean;
147
+ untrusted?: boolean;
64
148
  }
65
149
  export interface Database {
66
150
  name: string;
@@ -69,6 +153,11 @@ export interface Database {
69
153
  description?: string;
70
154
  /** When the source read its tables' stats, as an ISO timestamp: they change, so they carry their moment. */
71
155
  statsReadAt?: string;
156
+ /** The default collation; a column says its own only where it differs. */
157
+ collation?: string;
158
+ properties?: Properties;
159
+ /** What the source could not read and why, so a reader knows an absence is not a no. */
160
+ unread?: string[];
72
161
  schemas: Schema[];
73
162
  relations: Relation[];
74
163
  }
package/dist/model.js CHANGED
@@ -114,10 +114,15 @@ function checkDatabase(check, d, p) {
114
114
  check.optionalString(d.version, `${p}.version`);
115
115
  check.optionalString(d.description, `${p}.description`);
116
116
  check.optionalString(d.statsReadAt, `${p}.statsReadAt`);
117
+ check.optionalString(d.collation, `${p}.collation`);
118
+ check.properties(d.properties, `${p}.properties`);
119
+ if (d.unread !== undefined)
120
+ check.strings(d.unread, `${p}.unread`);
117
121
  check.list(d.schemas, `${p}.schemas`, (s, sp) => {
118
122
  if (!check.object(s, sp))
119
123
  return;
120
124
  check.string(s.name, `${sp}.name`);
125
+ check.properties(s.properties, `${sp}.properties`);
121
126
  check.list(s.tables, `${sp}.tables`, (t, tp) => checkTable(check, t, tp));
122
127
  });
123
128
  check.list(d.relations, `${p}.relations`, (r, rp) => {
@@ -132,6 +137,8 @@ function checkDatabase(check, d, p) {
132
137
  check.strings(e.columns, `${rp}.${end}.columns`);
133
138
  }
134
139
  check.optionalBoolean(r.virtual, `${rp}.virtual`);
140
+ check.optionalBoolean(r.disabled, `${rp}.disabled`);
141
+ check.optionalBoolean(r.untrusted, `${rp}.untrusted`);
135
142
  });
136
143
  }
137
144
  function checkTable(check, t, p) {
@@ -148,6 +155,7 @@ function checkTable(check, t, p) {
148
155
  }
149
156
  if (t.primaryKey !== undefined)
150
157
  check.strings(t.primaryKey, `${p}.primaryKey`);
158
+ checkCatalogue(check, t, p);
151
159
  check.list(t.columns, `${p}.columns`, (c, cp) => {
152
160
  if (!check.object(c, cp))
153
161
  return;
@@ -159,6 +167,18 @@ function checkTable(check, t, p) {
159
167
  check.optionalString(c.computed, `${cp}.computed`);
160
168
  check.optionalBoolean(c.identity, `${cp}.identity`);
161
169
  check.optionalString(c.description, `${cp}.description`);
170
+ for (const field of ['identitySeed', 'identityIncrement', 'collation', 'masked'])
171
+ check.optionalString(c[field], `${cp}.${field}`);
172
+ check.optionalBoolean(c.sparse, `${cp}.sparse`);
173
+ check.properties(c.properties, `${cp}.properties`);
174
+ if (c.encrypted !== undefined && check.object(c.encrypted, `${cp}.encrypted`)) {
175
+ check.oneOf(c.encrypted.type, ['deterministic', 'randomized'], `${cp}.encrypted.type`);
176
+ check.string(c.encrypted.key, `${cp}.encrypted.key`);
177
+ }
178
+ if (c.sensitivity !== undefined && check.object(c.sensitivity, `${cp}.sensitivity`)) {
179
+ for (const field of ['label', 'informationType', 'rank'])
180
+ check.optionalString(c.sensitivity[field], `${cp}.sensitivity.${field}`);
181
+ }
162
182
  });
163
183
  check.list(t.indexes, `${p}.indexes`, (x, xp) => {
164
184
  if (!check.object(x, xp))
@@ -167,6 +187,10 @@ function checkTable(check, t, p) {
167
187
  check.strings(x.columns, `${xp}.columns`);
168
188
  if (typeof x.unique !== 'boolean')
169
189
  check.errors.push(`${xp}.unique must be true or false`);
190
+ check.optionalString(x.type, `${xp}.type`);
191
+ check.optionalString(x.filter, `${xp}.filter`);
192
+ check.optionalBoolean(x.disabled, `${xp}.disabled`);
193
+ check.properties(x.properties, `${xp}.properties`);
170
194
  });
171
195
  check.list(t.constraints, `${p}.constraints`, (c, cp) => {
172
196
  if (!check.object(c, cp))
@@ -176,8 +200,56 @@ function checkTable(check, t, p) {
176
200
  check.errors.push(`${cp}.kind is not a constraint kind`);
177
201
  check.strings(c.columns, `${cp}.columns`);
178
202
  check.optionalString(c.definition, `${cp}.definition`);
203
+ check.optionalBoolean(c.disabled, `${cp}.disabled`);
204
+ check.optionalBoolean(c.untrusted, `${cp}.untrusted`);
205
+ check.properties(c.properties, `${cp}.properties`);
179
206
  });
180
207
  }
208
+ /** What the catalogue says about a table beyond its columns, keys and indexes. */
209
+ function checkCatalogue(check, t, p) {
210
+ check.properties(t.properties, `${p}.properties`);
211
+ for (const field of ['created', 'altered', 'historyOf', 'compression'])
212
+ check.optionalString(t[field], `${p}.${field}`);
213
+ for (const field of ['changeTracking', 'cdc'])
214
+ check.optionalBoolean(t[field], `${p}.${field}`);
215
+ if (t.memoryOptimised !== undefined)
216
+ check.oneOf(t.memoryOptimised, ['schema and data', 'schema only'], `${p}.memoryOptimised`);
217
+ if (t.ledger !== undefined)
218
+ check.oneOf(t.ledger, ['updatable', 'append-only', 'history'], `${p}.ledger`);
219
+ if (t.temporal !== undefined && check.object(t.temporal, `${p}.temporal`)) {
220
+ for (const field of ['history', 'start', 'end'])
221
+ check.string(t.temporal[field], `${p}.temporal.${field}`);
222
+ }
223
+ if (t.partitioning !== undefined && check.object(t.partitioning, `${p}.partitioning`)) {
224
+ for (const field of ['scheme', 'function', 'column'])
225
+ check.string(t.partitioning[field], `${p}.partitioning.${field}`);
226
+ check.count(t.partitioning.partitions, `${p}.partitioning.partitions`);
227
+ }
228
+ if (t.triggers !== undefined) {
229
+ check.list(t.triggers, `${p}.triggers`, (g, gp) => {
230
+ if (!check.object(g, gp))
231
+ return;
232
+ check.string(g.name, `${gp}.name`);
233
+ check.oneOf(g.timing, ['after', 'instead of'], `${gp}.timing`);
234
+ check.strings(g.events, `${gp}.events`);
235
+ if (typeof g.enabled !== 'boolean')
236
+ check.errors.push(`${gp}.enabled must be true or false`);
237
+ check.optionalString(g.definition, `${gp}.definition`);
238
+ });
239
+ }
240
+ if (t.security !== undefined) {
241
+ check.list(t.security, `${p}.security`, (r, rp) => {
242
+ if (!check.object(r, rp))
243
+ return;
244
+ check.string(r.policy, `${rp}.policy`);
245
+ check.oneOf(r.kind, ['filter', 'block'], `${rp}.kind`);
246
+ check.optionalString(r.operation, `${rp}.operation`);
247
+ check.string(r.predicate, `${rp}.predicate`);
248
+ if (typeof r.enabled !== 'boolean')
249
+ check.errors.push(`${rp}.enabled must be true or false`);
250
+ });
251
+ }
252
+ }
181
253
  class Checker {
182
254
  errors;
183
255
  constructor(errors) {
@@ -201,6 +273,18 @@ class Checker {
201
273
  if (value !== undefined && typeof value !== 'boolean')
202
274
  this.errors.push(`${path} must be true or false when given`);
203
275
  }
276
+ properties(value, path) {
277
+ if (value === undefined || !this.object(value, path))
278
+ return;
279
+ for (const [name, text] of Object.entries(value)) {
280
+ if (typeof text !== 'string')
281
+ this.errors.push(`${path}.${name} must be a string`);
282
+ }
283
+ }
284
+ oneOf(value, allowed, path) {
285
+ if (!allowed.includes(value))
286
+ this.errors.push(`${path} must be ${allowed.map((a) => `"${a}"`).join(' or ')}`);
287
+ }
204
288
  count(value, path) {
205
289
  if (typeof value !== 'number' || !Number.isFinite(value) || value < 0)
206
290
  this.errors.push(`${path} must be a number of 0 or more`);
@@ -1,8 +1,17 @@
1
- import { findTable, type Model, type TableStats } from '../model.ts';
1
+ import { findTable, type Model, type Table, type TableStats } from '../model.ts';
2
2
  export declare const wholeDiagramLimit = 40;
3
3
  export declare function renderMarkdown(model: Model): Record<string, string>;
4
4
  /** The product's name for a driver: mssql is SQL Server. */
5
5
  export declare function driverName(driver: string): string;
6
+ /** A thing the catalogue says about a table beyond its columns and keys: a name, and its particulars. */
7
+ export interface Feature {
8
+ name: string;
9
+ detail?: string;
10
+ /** A table the detail names, for a link. */
11
+ ref?: string;
12
+ }
13
+ /** What the catalogue says about a table beyond its columns and keys, in the order a reader weighs it. */
14
+ export declare function features(table: Table): Feature[];
6
15
  /** 1,204,377: thousands grouped the same way wherever it runs, so the docs do not change with the machine. */
7
16
  export declare function formatCount(n: number): string;
8
17
  /** 480 KB, 7.1 MB, 102 MB: binary units, as the database counts its pages, with one decimal below ten. */
@@ -27,6 +27,8 @@ function renderIndex(model) {
27
27
  if (database.description)
28
28
  parts.push(`${database.description}\n`);
29
29
  parts.push(`${driverLine(database)}\n`);
30
+ if (database.unread?.length)
31
+ parts.push(`Not read: ${database.unread.join('; ')}.\n`);
30
32
  // Rows and size only when the source read them; a view, which keeps no rows of its own, leaves them blank.
31
33
  const sized = database.schemas.some((s) => s.tables.some((t) => t.stats));
32
34
  parts.push(sized ? '| Table | Kind | Columns | Rows | Size | Description |\n|---|---|---|---|---|---|' : '| Table | Kind | Columns | Description |\n|---|---|---|---|');
@@ -39,6 +41,10 @@ function renderIndex(model) {
39
41
  }
40
42
  parts.push('');
41
43
  }
44
+ const owned = model.databases.flatMap((d) => [[d.name, d.properties], ...d.schemas.map((s) => [s.name, s.properties])]);
45
+ const properties = propertiesTable(owned);
46
+ if (properties)
47
+ parts.push(`## Properties\n\n${properties}`);
42
48
  for (const group of model.groups) {
43
49
  parts.push(`## ${group.name}\n`);
44
50
  if (group.description)
@@ -63,7 +69,81 @@ export function driverName(driver) {
63
69
  }
64
70
  function driverLine(database) {
65
71
  const name = driverName(database.driver);
66
- return database.version ? `${name} ${database.version}.` : `${name}.`;
72
+ const collation = database.collation ? `, collation ${database.collation}` : '';
73
+ return `${database.version ? `${name} ${database.version}` : name}${collation}.`;
74
+ }
75
+ /** What the catalogue says about a table beyond its columns and keys, in the order a reader weighs it. */
76
+ export function features(table) {
77
+ const list = [];
78
+ if (table.temporal) {
79
+ const { history, start, end } = table.temporal;
80
+ list.push({ name: 'System-versioned', detail: `history in ${history}, each row valid from ${start} to ${end}`, ref: history });
81
+ }
82
+ if (table.historyOf)
83
+ list.push({ name: 'History', detail: `of ${table.historyOf}`, ref: table.historyOf });
84
+ if (table.memoryOptimised)
85
+ list.push({ name: 'Memory-optimised', detail: table.memoryOptimised });
86
+ if (table.ledger)
87
+ list.push({ name: 'Ledger', detail: table.ledger });
88
+ if (table.partitioning) {
89
+ const { column, scheme, function: fn, partitions } = table.partitioning;
90
+ list.push({ name: 'Partitioned', detail: `on ${column} by ${scheme} (function ${fn}), ${partitions} partitions` });
91
+ }
92
+ // A list is a table whose partitions are compressed differently.
93
+ if (table.compression)
94
+ list.push({ name: 'Compressed', detail: table.compression.includes(',') ? `${table.compression}, by partition` : table.compression });
95
+ if (table.changeTracking)
96
+ list.push({ name: 'Change tracking' });
97
+ if (table.cdc)
98
+ list.push({ name: 'Change data capture' });
99
+ if (table.triggers?.length)
100
+ list.push({ name: 'Triggers', detail: String(table.triggers.length) });
101
+ if (table.security?.length)
102
+ list.push({ name: 'Row-level security', detail: [...new Set(table.security.map((s) => s.policy))].join(', ') });
103
+ if (table.created) {
104
+ const altered = table.altered && table.altered.slice(0, 10) !== table.created.slice(0, 10) ? `, last altered ${table.altered.slice(0, 10)}` : '';
105
+ list.push({ name: 'Created', detail: `${table.created.slice(0, 10)}${altered}` });
106
+ }
107
+ return list;
108
+ }
109
+ /** A column's type, with its collation where it differs from the database's. */
110
+ function columnType(column) {
111
+ return column.collation ? `${column.type}, collation ${column.collation}` : column.type;
112
+ }
113
+ /** What else the catalogue says about a column, for the Key cell: identity, sparse, masking, encryption. */
114
+ function columnTraits(column) {
115
+ const traits = [];
116
+ if (column.identity)
117
+ traits.push(column.identitySeed !== undefined ? `identity(${column.identitySeed},${column.identityIncrement ?? '1'})` : 'identity');
118
+ if (column.computed !== undefined)
119
+ traits.push(`computed \`${column.computed}\``);
120
+ if (column.sparse)
121
+ traits.push('sparse');
122
+ if (column.masked)
123
+ traits.push(`masked \`${column.masked}\``);
124
+ if (column.encrypted)
125
+ traits.push(`encrypted (${column.encrypted.type}, ${column.encrypted.key})`);
126
+ return traits;
127
+ }
128
+ /** The description cell: the description, then the sensitivity classification when there is one. */
129
+ function columnDescription(column) {
130
+ const parts = column.description ? [column.description] : [];
131
+ const s = column.sensitivity;
132
+ if (s)
133
+ parts.push(`Sensitivity: ${[s.label, s.informationType, s.rank && `rank ${s.rank}`].filter(Boolean).join(', ')}.`);
134
+ return parts.join(' ');
135
+ }
136
+ /** Not enforced, or not checked against the rows already there. */
137
+ function enforcement(item) {
138
+ return [item.disabled ? 'disabled' : '', item.untrusted ? 'not trusted' : ''].filter(Boolean);
139
+ }
140
+ function indexKind(index) {
141
+ return [index.type ?? '', index.filter ? `filtered \`${index.filter}\`` : '', index.disabled ? 'disabled' : ''].filter(Boolean).join(', ');
142
+ }
143
+ /** Every extended property of the things listed, one row each; empty when there are none. */
144
+ function propertiesTable(owners) {
145
+ const rows = owners.flatMap(([on, properties]) => Object.entries(properties ?? {}).map(([name, value]) => `| ${cell(on)} | ${cell(name)} | ${cell(value)} |`));
146
+ return rows.length === 0 ? '' : `| On | Name | Value |\n|---|---|---|\n${rows.join('\n')}\n`;
67
147
  }
68
148
  /** 1,204,377: thousands grouped the same way wherever it runs, so the docs do not change with the machine. */
69
149
  export function formatCount(n) {
@@ -99,6 +179,13 @@ function renderTable(model, ref, table, database) {
99
179
  parts.push(`${table.description}\n`);
100
180
  if (table.stats)
101
181
  parts.push(`${statsLine(table.stats, database.statsReadAt)}\n`);
182
+ const listed = features(table);
183
+ if (listed.length > 0) {
184
+ parts.push('## Features\n');
185
+ // A table the feature names gets a link when it is in the model, as a relation's would.
186
+ const linked = (f) => (f.ref && f.detail && findTable(model, f.ref) ? f.detail.replace(f.ref, `[${f.ref}](${f.ref}.md)`) : f.detail);
187
+ parts.push(listed.map((f) => `- **${f.name}**${f.detail ? `: ${linked(f)}` : ''}`).join('\n') + '\n');
188
+ }
102
189
  const relations = relationsOf(model, ref);
103
190
  parts.push('| Column | Type | Nullable | Default | Key | Description |\n|---|---|---|---|---|---|');
104
191
  for (const column of table.columns) {
@@ -110,16 +197,14 @@ function renderTable(model, ref, table, database) {
110
197
  if (i === -1)
111
198
  continue;
112
199
  const { table: toTable, columns } = relation.to;
113
- keys.push(`FK → [${toTable}](${toTable}.md).${columns[i] ?? columns.join(',')}${relation.virtual ? ' (notes)' : ''}`);
200
+ const state = [relation.virtual ? 'notes' : '', ...enforcement(relation)].filter(Boolean);
201
+ keys.push(`FK → [${toTable}](${toTable}.md).${columns[i] ?? columns.join(',')}${state.length ? ` (${state.join(', ')})` : ''}`);
114
202
  }
115
203
  if (isUniqueColumn(table, column.name))
116
204
  keys.push('unique');
117
- if (column.identity)
118
- keys.push('identity');
119
- if (column.computed !== undefined)
120
- keys.push(`computed \`${column.computed}\``);
205
+ keys.push(...columnTraits(column));
121
206
  const dflt = column.default === undefined ? '' : `\`${column.default}\``;
122
- parts.push(`| ${cell(column.name)} | ${cell(column.type)} | ${column.nullable ? 'yes' : 'no'} | ${cell(dflt)} | ${keys.join(', ')} | ${cell(column.description)} |`);
207
+ parts.push(`| ${cell(column.name)} | ${cell(columnType(column))} | ${column.nullable ? 'yes' : 'no'} | ${cell(dflt)} | ${cell(keys.join(', '))} | ${cell(columnDescription(column))} |`);
123
208
  }
124
209
  parts.push('');
125
210
  if (relations.in.length > 0) {
@@ -131,10 +216,12 @@ function renderTable(model, ref, table, database) {
131
216
  }
132
217
  if (table.indexes.length > 0) {
133
218
  parts.push('## Indexes\n');
134
- parts.push('| Name | Columns | Unique |\n|---|---|---|');
219
+ // The Kind column only when the source said what kind; a model without it renders as it always did.
220
+ const kinds = table.indexes.some((i) => indexKind(i) !== '');
221
+ parts.push(kinds ? '| Name | Columns | Unique | Kind |\n|---|---|---|---|' : '| Name | Columns | Unique |\n|---|---|---|');
135
222
  for (const index of table.indexes) {
136
223
  const columns = index.columns.join(', ') + (index.included?.length ? ` (includes ${index.included.join(', ')})` : '');
137
- parts.push(`| ${cell(index.name)} | ${cell(columns)} | ${index.unique ? 'yes' : 'no'} |`);
224
+ parts.push(`| ${cell(index.name)} | ${cell(columns)} | ${index.unique ? 'yes' : 'no'} |${kinds ? ` ${cell(indexKind(index))} |` : ''}`);
138
225
  }
139
226
  parts.push('');
140
227
  }
@@ -143,7 +230,34 @@ function renderTable(model, ref, table, database) {
143
230
  parts.push('| Name | Kind | Columns | Definition |\n|---|---|---|---|');
144
231
  for (const constraint of table.constraints) {
145
232
  const definition = constraint.definition === undefined ? '' : `\`${constraint.definition}\``;
146
- parts.push(`| ${cell(constraint.name)} | ${constraint.kind} | ${cell(constraint.columns.join(', '))} | ${cell(definition)} |`);
233
+ const kind = [constraint.kind, ...enforcement(constraint)].join(', ');
234
+ parts.push(`| ${cell(constraint.name)} | ${kind} | ${cell(constraint.columns.join(', '))} | ${cell(definition)} |`);
235
+ }
236
+ parts.push('');
237
+ }
238
+ const properties = propertiesTable([
239
+ [ref, table.properties],
240
+ ...table.columns.map((c) => [c.name, c.properties]),
241
+ ...table.indexes.map((i) => [i.name, i.properties]),
242
+ ...table.constraints.map((c) => [c.name, c.properties]),
243
+ ]);
244
+ if (properties)
245
+ parts.push(`## Properties\n\n${properties}`);
246
+ if (table.triggers?.length) {
247
+ parts.push('## Triggers\n');
248
+ for (const trigger of table.triggers) {
249
+ const when = `${trigger.timing === 'after' ? 'After' : 'Instead of'} ${trigger.events.join(', ')}. ${trigger.enabled ? 'Enabled' : 'Disabled'}.`;
250
+ parts.push(`### ${trigger.name}\n\n${when}\n`);
251
+ if (trigger.definition)
252
+ parts.push('```sql\n' + trigger.definition.trim() + '\n```\n');
253
+ }
254
+ }
255
+ if (table.security?.length) {
256
+ parts.push('## Row-level security\n');
257
+ parts.push('| Policy | Kind | Predicate | Enabled |\n|---|---|---|---|');
258
+ for (const s of table.security) {
259
+ const kind = s.operation ? `${s.kind}, ${s.operation}` : s.kind;
260
+ parts.push(`| ${cell(s.policy)} | ${kind} | ${cell(`\`${s.predicate}\``)} | ${s.enabled ? 'yes' : 'no'} |`);
147
261
  }
148
262
  parts.push('');
149
263
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dbtour",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Tour your database and its API: searchable docs, relations and diagrams, generated from the schema, kept in the repo.",
5
5
  "type": "module",
6
6
  "license": "MIT",