datanotes 3.4.1 → 3.4.2
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/api/extension-columns.d.cts +64 -0
- package/dist/api/extension-columns.d.ts +64 -0
- package/dist/api/extension-plan.d.cts +1 -1
- package/dist/api/extension-plan.d.ts +1 -1
- package/dist/datanotes.cjs +270 -24
- package/dist/docs/administration/extensions.md +2 -1
- package/dist/docs/api.md +1 -1
- package/dist/docs/develop/write-an-extension.md +8 -2
- package/dist/docs/guides/data-quality/check-constraints.md +10 -10
- package/dist/docs/guides/data-quality/hand-edits.md +8 -8
- package/dist/docs/guides/database-objects/bases.md +11 -11
- package/dist/docs/guides/database-objects/catalogs-and-schemas.md +10 -10
- package/dist/docs/guides/database-objects/columns.md +39 -39
- package/dist/docs/guides/database-objects/relations.md +28 -28
- package/dist/docs/guides/database-objects/tables.md +26 -26
- package/dist/docs/guides/database-objects/views.md +19 -19
- package/dist/docs/guides/history/change-data-feed.md +34 -34
- package/dist/docs/guides/history/journal-and-undo.md +12 -12
- package/dist/docs/guides/history/restore.md +20 -20
- package/dist/docs/guides/history/table-history.md +31 -31
- package/dist/docs/guides/read-and-write/automatic-links.md +15 -15
- package/dist/docs/guides/read-and-write/import-notes.md +20 -20
- package/dist/docs/guides/read-and-write/write-rows.md +10 -10
- package/dist/docs/guides/search/text.md +1 -1
- package/dist/docs/reference/cli.md +1 -1
- package/dist/docs/release-notes.md +13 -0
- package/package.json +1 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Column bindings of an extension: the manifest columns of a dependency role may be bound to
|
|
3
|
+
* columns of another name in the user's table (`--map role.column=column`). The manifest names
|
|
4
|
+
* columns in its view queries, CHECKs and link filters; this module rewrites those names to the
|
|
5
|
+
* bound ones, following link columns from role to role (`book.author.name`). Pure: no I/O.
|
|
6
|
+
*
|
|
7
|
+
* What it cannot rewrite safely it reports as an issue (fail closed), never guesses: a mapped
|
|
8
|
+
* column named in the outer clauses of a union view, or under a query key it does not know.
|
|
9
|
+
*/
|
|
10
|
+
import type { ExtensionManifest, ManifestColumn } from "./extension-manifest.cjs";
|
|
11
|
+
/** role → (manifest column → bound column); only the columns whose names differ matter. */
|
|
12
|
+
export type ColumnMap = Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
13
|
+
/** A place where a mapped column is named and cannot be rewritten. */
|
|
14
|
+
export interface ColumnIssue {
|
|
15
|
+
role: string;
|
|
16
|
+
column: string;
|
|
17
|
+
/** Where it is named: `view vw_x (union order_by)`. */
|
|
18
|
+
where: string;
|
|
19
|
+
}
|
|
20
|
+
type Obj = Record<string, unknown>;
|
|
21
|
+
/** Rewrites the column names of bound roles; `issues` collects what it could not rewrite. */
|
|
22
|
+
export declare class ColumnBinder {
|
|
23
|
+
private readonly manifest;
|
|
24
|
+
readonly issues: ColumnIssue[];
|
|
25
|
+
private readonly map;
|
|
26
|
+
constructor(manifest: Pick<ExtensionManifest, "tables">, map: ColumnMap);
|
|
27
|
+
/** True when no role has a column bound to another name: every rewrite is the identity. */
|
|
28
|
+
get empty(): boolean;
|
|
29
|
+
/** The bound name of a role's manifest column. */
|
|
30
|
+
name(role: string | null, column: string): string;
|
|
31
|
+
/** The manifest columns of a role bound to another name. */
|
|
32
|
+
mapped(role: string): string[];
|
|
33
|
+
/** The role a link column of `role` points to (`ref: "@x"`, or `self`), or null. */
|
|
34
|
+
private linkRole;
|
|
35
|
+
/** A dotted column path read on `role`: each part on the role the previous link points to. */
|
|
36
|
+
path(role: string | null, dotted: string): string;
|
|
37
|
+
/** A group_by or join expression: `col`, `path`, `date(col)`, `date(col)+1`. */
|
|
38
|
+
expr(role: string | null, e: string): string;
|
|
39
|
+
/** A `where` filter (or a CHECK) on `role`: keys are column paths; `or` / `and` / `not` nest. */
|
|
40
|
+
where(role: string | null, where: unknown): unknown;
|
|
41
|
+
/** A column spec of `role`: its link filter (`ref_where`), `ref_by`, `inverse`. */
|
|
42
|
+
column(role: string, c: ManifestColumn): ManifestColumn;
|
|
43
|
+
/** A view query: the tables it reads are roles (`@meal`); every column it names follows them. */
|
|
44
|
+
query(view: string, q: Obj): Obj;
|
|
45
|
+
private order;
|
|
46
|
+
/** `count`, `count:col`, `sum:meal.kcal`. */
|
|
47
|
+
private aggregate;
|
|
48
|
+
/** `{table, on: {left expr: right expr}, as, type, columns, where}`: `columns` and `where` read the joined table. */
|
|
49
|
+
private join;
|
|
50
|
+
/** `{union: [{table, columns, where}], …}`: each part on its role; the outer clauses read every part, so a mapped column there is an issue. */
|
|
51
|
+
private union;
|
|
52
|
+
/** A clause the binder does not rewrite: an issue for each mapped column it names. */
|
|
53
|
+
private unknown;
|
|
54
|
+
/**
|
|
55
|
+
* The mapped columns a value names, where nothing rewrites it: a word that is a mapped column of
|
|
56
|
+
* one of `roles`, or a later part of a dotted path (`author.name`) that is a mapped column of any
|
|
57
|
+
* role. `keys(key, inKeys)` tells whether the keys under `key` are names too.
|
|
58
|
+
*/
|
|
59
|
+
named(v: unknown, roles: string[], keys: (key: string, inKeys: boolean) => boolean): {
|
|
60
|
+
role: string;
|
|
61
|
+
column: string;
|
|
62
|
+
}[];
|
|
63
|
+
}
|
|
64
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Column bindings of an extension: the manifest columns of a dependency role may be bound to
|
|
3
|
+
* columns of another name in the user's table (`--map role.column=column`). The manifest names
|
|
4
|
+
* columns in its view queries, CHECKs and link filters; this module rewrites those names to the
|
|
5
|
+
* bound ones, following link columns from role to role (`book.author.name`). Pure: no I/O.
|
|
6
|
+
*
|
|
7
|
+
* What it cannot rewrite safely it reports as an issue (fail closed), never guesses: a mapped
|
|
8
|
+
* column named in the outer clauses of a union view, or under a query key it does not know.
|
|
9
|
+
*/
|
|
10
|
+
import type { ExtensionManifest, ManifestColumn } from "./extension-manifest.js";
|
|
11
|
+
/** role → (manifest column → bound column); only the columns whose names differ matter. */
|
|
12
|
+
export type ColumnMap = Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
13
|
+
/** A place where a mapped column is named and cannot be rewritten. */
|
|
14
|
+
export interface ColumnIssue {
|
|
15
|
+
role: string;
|
|
16
|
+
column: string;
|
|
17
|
+
/** Where it is named: `view vw_x (union order_by)`. */
|
|
18
|
+
where: string;
|
|
19
|
+
}
|
|
20
|
+
type Obj = Record<string, unknown>;
|
|
21
|
+
/** Rewrites the column names of bound roles; `issues` collects what it could not rewrite. */
|
|
22
|
+
export declare class ColumnBinder {
|
|
23
|
+
private readonly manifest;
|
|
24
|
+
readonly issues: ColumnIssue[];
|
|
25
|
+
private readonly map;
|
|
26
|
+
constructor(manifest: Pick<ExtensionManifest, "tables">, map: ColumnMap);
|
|
27
|
+
/** True when no role has a column bound to another name: every rewrite is the identity. */
|
|
28
|
+
get empty(): boolean;
|
|
29
|
+
/** The bound name of a role's manifest column. */
|
|
30
|
+
name(role: string | null, column: string): string;
|
|
31
|
+
/** The manifest columns of a role bound to another name. */
|
|
32
|
+
mapped(role: string): string[];
|
|
33
|
+
/** The role a link column of `role` points to (`ref: "@x"`, or `self`), or null. */
|
|
34
|
+
private linkRole;
|
|
35
|
+
/** A dotted column path read on `role`: each part on the role the previous link points to. */
|
|
36
|
+
path(role: string | null, dotted: string): string;
|
|
37
|
+
/** A group_by or join expression: `col`, `path`, `date(col)`, `date(col)+1`. */
|
|
38
|
+
expr(role: string | null, e: string): string;
|
|
39
|
+
/** A `where` filter (or a CHECK) on `role`: keys are column paths; `or` / `and` / `not` nest. */
|
|
40
|
+
where(role: string | null, where: unknown): unknown;
|
|
41
|
+
/** A column spec of `role`: its link filter (`ref_where`), `ref_by`, `inverse`. */
|
|
42
|
+
column(role: string, c: ManifestColumn): ManifestColumn;
|
|
43
|
+
/** A view query: the tables it reads are roles (`@meal`); every column it names follows them. */
|
|
44
|
+
query(view: string, q: Obj): Obj;
|
|
45
|
+
private order;
|
|
46
|
+
/** `count`, `count:col`, `sum:meal.kcal`. */
|
|
47
|
+
private aggregate;
|
|
48
|
+
/** `{table, on: {left expr: right expr}, as, type, columns, where}`: `columns` and `where` read the joined table. */
|
|
49
|
+
private join;
|
|
50
|
+
/** `{union: [{table, columns, where}], …}`: each part on its role; the outer clauses read every part, so a mapped column there is an issue. */
|
|
51
|
+
private union;
|
|
52
|
+
/** A clause the binder does not rewrite: an issue for each mapped column it names. */
|
|
53
|
+
private unknown;
|
|
54
|
+
/**
|
|
55
|
+
* The mapped columns a value names, where nothing rewrites it: a word that is a mapped column of
|
|
56
|
+
* one of `roles`, or a later part of a dotted path (`author.name`) that is a mapped column of any
|
|
57
|
+
* role. `keys(key, inKeys)` tells whether the keys under `key` are names too.
|
|
58
|
+
*/
|
|
59
|
+
named(v: unknown, roles: string[], keys: (key: string, inKeys: boolean) => boolean): {
|
|
60
|
+
role: string;
|
|
61
|
+
column: string;
|
|
62
|
+
}[];
|
|
63
|
+
}
|
|
64
|
+
export {};
|
|
@@ -48,7 +48,7 @@ export interface TableChoice {
|
|
|
48
48
|
proposed: boolean;
|
|
49
49
|
candidates: string[];
|
|
50
50
|
}
|
|
51
|
-
export type ProblemCode = "engine_too_old" | "unknown_role" | "table_not_found" | "table_missing" | "table_exists" | "owned_create" | "ambiguous_dependency" | "missing_column" | "column_differs" | "owned_column_map" | "migration_conflict" | "base_differs" | "view_exists" | "tool_name_taken" | "token_exists" | "too_many_operations" | "already_installed" | "downgrade" | "same_version";
|
|
51
|
+
export type ProblemCode = "engine_too_old" | "unknown_role" | "table_not_found" | "table_missing" | "table_exists" | "owned_create" | "ambiguous_dependency" | "missing_column" | "column_differs" | "owned_column_map" | "column_map_unsupported" | "migration_conflict" | "base_differs" | "view_exists" | "tool_name_taken" | "token_exists" | "too_many_operations" | "already_installed" | "downgrade" | "same_version";
|
|
52
52
|
/** Something that keeps the plan from being written: what is wrong, how to fix it, where to read more. */
|
|
53
53
|
export interface PlanProblem {
|
|
54
54
|
code: ProblemCode;
|
|
@@ -48,7 +48,7 @@ export interface TableChoice {
|
|
|
48
48
|
proposed: boolean;
|
|
49
49
|
candidates: string[];
|
|
50
50
|
}
|
|
51
|
-
export type ProblemCode = "engine_too_old" | "unknown_role" | "table_not_found" | "table_missing" | "table_exists" | "owned_create" | "ambiguous_dependency" | "missing_column" | "column_differs" | "owned_column_map" | "migration_conflict" | "base_differs" | "view_exists" | "tool_name_taken" | "token_exists" | "too_many_operations" | "already_installed" | "downgrade" | "same_version";
|
|
51
|
+
export type ProblemCode = "engine_too_old" | "unknown_role" | "table_not_found" | "table_missing" | "table_exists" | "owned_create" | "ambiguous_dependency" | "missing_column" | "column_differs" | "owned_column_map" | "column_map_unsupported" | "migration_conflict" | "base_differs" | "view_exists" | "tool_name_taken" | "token_exists" | "too_many_operations" | "already_installed" | "downgrade" | "same_version";
|
|
52
52
|
/** Something that keeps the plan from being written: what is wrong, how to fix it, where to read more. */
|
|
53
53
|
export interface PlanProblem {
|
|
54
54
|
code: ProblemCode;
|
package/dist/datanotes.cjs
CHANGED
|
@@ -38937,11 +38937,223 @@ var init_extension_host = __esm({
|
|
|
38937
38937
|
}
|
|
38938
38938
|
});
|
|
38939
38939
|
|
|
38940
|
+
// src/api/extension-columns.ts
|
|
38941
|
+
function roleOf(v) {
|
|
38942
|
+
if (typeof v !== "string") return null;
|
|
38943
|
+
const m = ROLE_REF.exec(v);
|
|
38944
|
+
return m ? m[1] : null;
|
|
38945
|
+
}
|
|
38946
|
+
var isObject5, LOGICAL2, DATE_EXPR3, QUERY_KEYS, JOIN_KEYS, UNION_PART_KEYS, ColumnBinder;
|
|
38947
|
+
var init_extension_columns = __esm({
|
|
38948
|
+
"src/api/extension-columns.ts"() {
|
|
38949
|
+
"use strict";
|
|
38950
|
+
init_extension_manifest();
|
|
38951
|
+
isObject5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
38952
|
+
LOGICAL2 = /* @__PURE__ */ new Set(["or", "and", "not"]);
|
|
38953
|
+
DATE_EXPR3 = /^date\((.+)\)([+-]\d+)?$/;
|
|
38954
|
+
QUERY_KEYS = /* @__PURE__ */ new Set(["table", "columns", "where", "order_by", "limit", "offset", "expand", "join", "group_by", "aggregates", "include_body", "union"]);
|
|
38955
|
+
JOIN_KEYS = /* @__PURE__ */ new Set(["table", "on", "as", "type", "columns", "where"]);
|
|
38956
|
+
UNION_PART_KEYS = /* @__PURE__ */ new Set(["table", "columns", "where"]);
|
|
38957
|
+
ColumnBinder = class {
|
|
38958
|
+
constructor(manifest, map) {
|
|
38959
|
+
this.manifest = manifest;
|
|
38960
|
+
this.issues = [];
|
|
38961
|
+
this.map = {};
|
|
38962
|
+
for (const [role, cols] of Object.entries(map)) {
|
|
38963
|
+
const differing = Object.entries(cols).filter(([from, to]) => from !== to);
|
|
38964
|
+
if (differing.length > 0) this.map[role] = Object.fromEntries(differing);
|
|
38965
|
+
}
|
|
38966
|
+
}
|
|
38967
|
+
/** True when no role has a column bound to another name: every rewrite is the identity. */
|
|
38968
|
+
get empty() {
|
|
38969
|
+
return Object.keys(this.map).length === 0;
|
|
38970
|
+
}
|
|
38971
|
+
/** The bound name of a role's manifest column. */
|
|
38972
|
+
name(role, column) {
|
|
38973
|
+
return role === null ? column : this.map[role]?.[column] ?? column;
|
|
38974
|
+
}
|
|
38975
|
+
/** The manifest columns of a role bound to another name. */
|
|
38976
|
+
mapped(role) {
|
|
38977
|
+
return Object.keys(this.map[role] ?? {});
|
|
38978
|
+
}
|
|
38979
|
+
/** The role a link column of `role` points to (`ref: "@x"`, or `self`), or null. */
|
|
38980
|
+
linkRole(role, column) {
|
|
38981
|
+
const c = typeof column === "string" ? this.manifest.tables[role]?.columns.find((x) => x.name === column) : column;
|
|
38982
|
+
if (!c || c.ref === void 0) return null;
|
|
38983
|
+
if (!(c.type === "link" || c.type === "list" && c.items === "link")) return null;
|
|
38984
|
+
return c.ref === "self" ? role : roleOf(c.ref);
|
|
38985
|
+
}
|
|
38986
|
+
/** A dotted column path read on `role`: each part on the role the previous link points to. */
|
|
38987
|
+
path(role, dotted) {
|
|
38988
|
+
if (role === null || this.empty) return dotted;
|
|
38989
|
+
const parts = dotted.split(".");
|
|
38990
|
+
const out2 = [];
|
|
38991
|
+
let cur = role;
|
|
38992
|
+
for (const part of parts) {
|
|
38993
|
+
out2.push(this.name(cur, part));
|
|
38994
|
+
cur = cur === null ? null : this.linkRole(cur, part);
|
|
38995
|
+
}
|
|
38996
|
+
return out2.join(".");
|
|
38997
|
+
}
|
|
38998
|
+
/** A group_by or join expression: `col`, `path`, `date(col)`, `date(col)+1`. */
|
|
38999
|
+
expr(role, e) {
|
|
39000
|
+
const d = DATE_EXPR3.exec(e);
|
|
39001
|
+
return d ? `date(${this.path(role, d[1])})${d[2] ?? ""}` : this.path(role, e);
|
|
39002
|
+
}
|
|
39003
|
+
/** A `where` filter (or a CHECK) on `role`: keys are column paths; `or` / `and` / `not` nest. */
|
|
39004
|
+
where(role, where) {
|
|
39005
|
+
if (!isObject5(where) || role === null || this.empty) return where;
|
|
39006
|
+
const out2 = {};
|
|
39007
|
+
for (const [k, v] of Object.entries(where)) {
|
|
39008
|
+
if (LOGICAL2.has(k)) out2[k] = Array.isArray(v) ? v.map((w) => this.where(role, w)) : this.where(role, v);
|
|
39009
|
+
else out2[this.path(role, k)] = v;
|
|
39010
|
+
}
|
|
39011
|
+
return out2;
|
|
39012
|
+
}
|
|
39013
|
+
/** A column spec of `role`: its link filter (`ref_where`), `ref_by`, `inverse`. */
|
|
39014
|
+
column(role, c) {
|
|
39015
|
+
if (this.empty) return c;
|
|
39016
|
+
let out2 = c;
|
|
39017
|
+
const target = this.linkRole(role, c);
|
|
39018
|
+
if (c.ref_where !== void 0) out2 = { ...out2, ref_where: this.where(target, c.ref_where) };
|
|
39019
|
+
if (isObject5(c.ref_by)) {
|
|
39020
|
+
const by = c.ref_by;
|
|
39021
|
+
const col = this.name(role, String(by.column));
|
|
39022
|
+
const tgt = this.name(target, String(by.target_column ?? by.column));
|
|
39023
|
+
const { target_column: _t, ...rest } = by;
|
|
39024
|
+
out2 = { ...out2, ref_by: { ...rest, column: col, ...tgt !== col ? { target_column: tgt } : {} } };
|
|
39025
|
+
}
|
|
39026
|
+
if (typeof c.inverse === "string") out2 = { ...out2, inverse: this.name(target, c.inverse) };
|
|
39027
|
+
return out2;
|
|
39028
|
+
}
|
|
39029
|
+
/** A view query: the tables it reads are roles (`@meal`); every column it names follows them. */
|
|
39030
|
+
query(view, q) {
|
|
39031
|
+
if (this.empty) return q;
|
|
39032
|
+
if (Array.isArray(q.union)) return this.union(view, q);
|
|
39033
|
+
const role = roleOf(q.table);
|
|
39034
|
+
const out2 = {};
|
|
39035
|
+
const aliases = new Set(isObject5(q.aggregates) ? Object.keys(q.aggregates) : []);
|
|
39036
|
+
const col = (v) => aliases.has(v) ? v : this.path(role, v);
|
|
39037
|
+
for (const [k, v] of Object.entries(q)) {
|
|
39038
|
+
switch (k) {
|
|
39039
|
+
case "columns":
|
|
39040
|
+
case "expand":
|
|
39041
|
+
out2[k] = Array.isArray(v) ? v.map((x) => typeof x === "string" ? col(x) : x) : v;
|
|
39042
|
+
break;
|
|
39043
|
+
case "order_by":
|
|
39044
|
+
out2[k] = Array.isArray(v) ? v.map((x) => this.order(x, col)) : this.order(v, col);
|
|
39045
|
+
break;
|
|
39046
|
+
case "where":
|
|
39047
|
+
out2[k] = this.where(role, v);
|
|
39048
|
+
break;
|
|
39049
|
+
case "group_by":
|
|
39050
|
+
out2[k] = Array.isArray(v) ? v.map((x) => typeof x === "string" ? this.expr(role, x) : x) : typeof v === "string" ? this.expr(role, v) : v;
|
|
39051
|
+
break;
|
|
39052
|
+
case "aggregates":
|
|
39053
|
+
out2[k] = isObject5(v) ? Object.fromEntries(Object.entries(v).map(([a, spec2]) => [a, this.aggregate(role, spec2)])) : v;
|
|
39054
|
+
break;
|
|
39055
|
+
case "join":
|
|
39056
|
+
out2[k] = isObject5(v) ? this.join(view, role, v) : v;
|
|
39057
|
+
break;
|
|
39058
|
+
default:
|
|
39059
|
+
if (!QUERY_KEYS.has(k)) this.unknown(view, [role], k, v);
|
|
39060
|
+
out2[k] = v;
|
|
39061
|
+
}
|
|
39062
|
+
}
|
|
39063
|
+
return out2;
|
|
39064
|
+
}
|
|
39065
|
+
order(v, col) {
|
|
39066
|
+
if (typeof v !== "string") return v;
|
|
39067
|
+
return v.startsWith("-") ? `-${col(v.slice(1))}` : col(v);
|
|
39068
|
+
}
|
|
39069
|
+
/** `count`, `count:col`, `sum:meal.kcal`. */
|
|
39070
|
+
aggregate(role, spec2) {
|
|
39071
|
+
if (typeof spec2 !== "string") return spec2;
|
|
39072
|
+
const i = spec2.indexOf(":");
|
|
39073
|
+
return i < 0 ? spec2 : `${spec2.slice(0, i)}:${this.path(role, spec2.slice(i + 1))}`;
|
|
39074
|
+
}
|
|
39075
|
+
/** `{table, on: {left expr: right expr}, as, type, columns, where}`: `columns` and `where` read the joined table. */
|
|
39076
|
+
join(view, left, j) {
|
|
39077
|
+
const right = roleOf(j.table);
|
|
39078
|
+
const out2 = {};
|
|
39079
|
+
for (const [k, v] of Object.entries(j)) {
|
|
39080
|
+
if (k === "on" && isObject5(v)) {
|
|
39081
|
+
out2[k] = Object.fromEntries(Object.entries(v).map(([a, b]) => [this.expr(left, a), typeof b === "string" ? this.expr(right, b) : b]));
|
|
39082
|
+
} else if (k === "columns" && Array.isArray(v)) out2[k] = v.map((x) => typeof x === "string" ? this.path(right, x) : x);
|
|
39083
|
+
else if (k === "where") out2[k] = this.where(right, v);
|
|
39084
|
+
else {
|
|
39085
|
+
if (!JOIN_KEYS.has(k)) this.unknown(view, [left, right], `join.${k}`, v);
|
|
39086
|
+
out2[k] = v;
|
|
39087
|
+
}
|
|
39088
|
+
}
|
|
39089
|
+
return out2;
|
|
39090
|
+
}
|
|
39091
|
+
/** `{union: [{table, columns, where}], …}`: each part on its role; the outer clauses read every part, so a mapped column there is an issue. */
|
|
39092
|
+
union(view, q) {
|
|
39093
|
+
const roles = [];
|
|
39094
|
+
const parts = q.union.map((p) => {
|
|
39095
|
+
if (!isObject5(p)) return p;
|
|
39096
|
+
const role = roleOf(p.table);
|
|
39097
|
+
roles.push(role);
|
|
39098
|
+
const out2 = {};
|
|
39099
|
+
for (const [k, v] of Object.entries(p)) {
|
|
39100
|
+
if (k === "columns" && Array.isArray(v)) out2[k] = v.map((x) => typeof x === "string" ? this.path(role, x) : x);
|
|
39101
|
+
else if (k === "where") out2[k] = this.where(role, v);
|
|
39102
|
+
else {
|
|
39103
|
+
if (!UNION_PART_KEYS.has(k)) this.unknown(view, [role], `union.${k}`, v);
|
|
39104
|
+
out2[k] = v;
|
|
39105
|
+
}
|
|
39106
|
+
}
|
|
39107
|
+
return out2;
|
|
39108
|
+
});
|
|
39109
|
+
for (const [k, v] of Object.entries(q)) if (k !== "union") this.unknown(view, roles, `union ${k}`, v);
|
|
39110
|
+
return { ...q, union: parts };
|
|
39111
|
+
}
|
|
39112
|
+
/** A clause the binder does not rewrite: an issue for each mapped column it names. */
|
|
39113
|
+
unknown(view, roles, key2, v) {
|
|
39114
|
+
for (const n of this.named(v, roles.filter((r) => r !== null), () => true)) this.issues.push({ ...n, where: `view ${view} (${key2})` });
|
|
39115
|
+
}
|
|
39116
|
+
/**
|
|
39117
|
+
* The mapped columns a value names, where nothing rewrites it: a word that is a mapped column of
|
|
39118
|
+
* one of `roles`, or a later part of a dotted path (`author.name`) that is a mapped column of any
|
|
39119
|
+
* role. `keys(key, inKeys)` tells whether the keys under `key` are names too.
|
|
39120
|
+
*/
|
|
39121
|
+
named(v, roles, keys) {
|
|
39122
|
+
const words = /* @__PURE__ */ new Set();
|
|
39123
|
+
const tails = /* @__PURE__ */ new Set();
|
|
39124
|
+
const text5 = (x) => {
|
|
39125
|
+
for (const w of x.split(/[^A-Za-z0-9_.]+/)) {
|
|
39126
|
+
const parts = w.split(".");
|
|
39127
|
+
if (parts.length === 1) words.add(w);
|
|
39128
|
+
else for (const p of parts.slice(1)) tails.add(p);
|
|
39129
|
+
}
|
|
39130
|
+
};
|
|
39131
|
+
const walk = (x, inKeys) => {
|
|
39132
|
+
if (typeof x === "string") text5(x);
|
|
39133
|
+
else if (Array.isArray(x)) x.forEach((y) => walk(y, inKeys));
|
|
39134
|
+
else if (isObject5(x)) {
|
|
39135
|
+
for (const [k, y] of Object.entries(x)) {
|
|
39136
|
+
if (inKeys) text5(k);
|
|
39137
|
+
walk(y, keys(k, inKeys));
|
|
39138
|
+
}
|
|
39139
|
+
}
|
|
39140
|
+
};
|
|
39141
|
+
walk(v, keys("", false));
|
|
39142
|
+
const out2 = [];
|
|
39143
|
+
for (const role of Object.keys(this.map)) {
|
|
39144
|
+
for (const column of this.mapped(role)) if (tails.has(column) || roles.includes(role) && words.has(column)) out2.push({ role, column });
|
|
39145
|
+
}
|
|
39146
|
+
return out2;
|
|
39147
|
+
}
|
|
39148
|
+
};
|
|
39149
|
+
}
|
|
39150
|
+
});
|
|
39151
|
+
|
|
38940
39152
|
// src/api/extension-plan.ts
|
|
38941
39153
|
function stable(v) {
|
|
38942
39154
|
const walk = (x) => {
|
|
38943
39155
|
if (Array.isArray(x)) return x.map(walk);
|
|
38944
|
-
if (
|
|
39156
|
+
if (isObject6(x)) {
|
|
38945
39157
|
const out2 = {};
|
|
38946
39158
|
for (const k of Object.keys(x).sort()) out2[k] = walk(x[k]);
|
|
38947
39159
|
return out2;
|
|
@@ -39115,6 +39327,10 @@ function planChanges(input) {
|
|
|
39115
39327
|
for (const role of m.roles) tables[role] = choiceOf.get(role)?.table ?? `@${role}`;
|
|
39116
39328
|
const bind = (v) => bindRoles(v, tables);
|
|
39117
39329
|
const existing = (c) => c.decision === "adopt" || c.decision === "match";
|
|
39330
|
+
const columnMap = {};
|
|
39331
|
+
for (const c of input.chosen) if (!c.owned && c.decision === "match") columnMap[c.role] = { ...recorded[c.role]?.columns ?? {}, ...choices.columns[c.role] ?? {} };
|
|
39332
|
+
const binder = new ColumnBinder(m, columnMap);
|
|
39333
|
+
const bindColumn = (role, c) => bind(spec(binder.column(role, c)));
|
|
39118
39334
|
const model = /* @__PURE__ */ new Map();
|
|
39119
39335
|
for (const c of input.chosen) if (existing(c)) model.set(c.table, modelTable(input.described[c.table] ?? { qualified: c.table, columns: [] }));
|
|
39120
39336
|
const views = new Set(input.listing.views.map((v) => v.qualified));
|
|
@@ -39142,6 +39358,19 @@ function planChanges(input) {
|
|
|
39142
39358
|
);
|
|
39143
39359
|
continue;
|
|
39144
39360
|
}
|
|
39361
|
+
const touched = mappedNamed(raw);
|
|
39362
|
+
if (touched !== null) {
|
|
39363
|
+
migrationProblems.push(
|
|
39364
|
+
problem(
|
|
39365
|
+
"migration_conflict",
|
|
39366
|
+
`migration ${g.version}: ${raw.op} names column ${touched.column} of ${touched.role}, which reads your column ${binder.name(touched.role, touched.column)}`,
|
|
39367
|
+
`Rename your column to ${touched.column} with rename_column and run the command again without --map ${touched.role}.${touched.column}, or ask the extension's author.`,
|
|
39368
|
+
EXT_DOCS.update,
|
|
39369
|
+
{ role: touched.role, column: touched.column }
|
|
39370
|
+
)
|
|
39371
|
+
);
|
|
39372
|
+
continue;
|
|
39373
|
+
}
|
|
39145
39374
|
const op2 = bind(raw);
|
|
39146
39375
|
const r = simulate(op2, g.version);
|
|
39147
39376
|
if (typeof r === "string") {
|
|
@@ -39153,6 +39382,9 @@ function planChanges(input) {
|
|
|
39153
39382
|
}
|
|
39154
39383
|
}
|
|
39155
39384
|
}
|
|
39385
|
+
function mappedNamed(raw) {
|
|
39386
|
+
return binder.named(raw, rolesNamed(raw), (k, inKeys) => inKeys || FILTER_KEYS.has(k))[0] ?? null;
|
|
39387
|
+
}
|
|
39156
39388
|
function qualify(op2) {
|
|
39157
39389
|
return `${String(op2.catalog ?? catalog)}.${String(op2.schema ?? viewSchema)}.${String(op2.name)}`;
|
|
39158
39390
|
}
|
|
@@ -39285,7 +39517,7 @@ function planChanges(input) {
|
|
|
39285
39517
|
schema,
|
|
39286
39518
|
name: tname,
|
|
39287
39519
|
description: def.description,
|
|
39288
|
-
columns:
|
|
39520
|
+
columns: def.columns.map((c) => bindColumn(role, c)),
|
|
39289
39521
|
...def.filename !== void 0 ? { filename: def.filename } : {},
|
|
39290
39522
|
...def.aliases !== void 0 ? { aliases: def.aliases } : {}
|
|
39291
39523
|
});
|
|
@@ -39294,8 +39526,8 @@ function planChanges(input) {
|
|
|
39294
39526
|
}
|
|
39295
39527
|
const actualCols = model.get(q).columns;
|
|
39296
39528
|
for (const c of def.columns) {
|
|
39297
|
-
const bound =
|
|
39298
|
-
const actualName =
|
|
39529
|
+
const bound = bindColumn(role, c);
|
|
39530
|
+
const actualName = binder.name(role, c.name);
|
|
39299
39531
|
const actual = actualCols.get(actualName);
|
|
39300
39532
|
if (actual) {
|
|
39301
39533
|
const diff = columnDifference(bound, actual, q, false);
|
|
@@ -39336,7 +39568,7 @@ function planChanges(input) {
|
|
|
39336
39568
|
const q = t.table;
|
|
39337
39569
|
const tags = { extension: name, extension_role: role };
|
|
39338
39570
|
const instructions = def.instructions === void 0 ? void 0 : bind(def.instructions);
|
|
39339
|
-
const constraint = (c) => ({ op: "add_constraint", table: q, name: c.name, check: bind(c.check), hint: c.hint });
|
|
39571
|
+
const constraint = (c) => ({ op: "add_constraint", table: q, name: c.name, check: bind(binder.where(role, c.check)), hint: c.hint });
|
|
39340
39572
|
if (t.decision === "create") {
|
|
39341
39573
|
const [cat, schema, tname] = q.split(".");
|
|
39342
39574
|
operations.push({
|
|
@@ -39345,7 +39577,7 @@ function planChanges(input) {
|
|
|
39345
39577
|
schema,
|
|
39346
39578
|
name: tname,
|
|
39347
39579
|
description: def.description,
|
|
39348
|
-
columns:
|
|
39580
|
+
columns: def.columns.map((c) => bindColumn(role, c)),
|
|
39349
39581
|
...def.filename !== void 0 ? { filename: def.filename } : {},
|
|
39350
39582
|
...def.aliases !== void 0 ? { aliases: def.aliases } : {},
|
|
39351
39583
|
...instructions !== void 0 ? { instructions } : {},
|
|
@@ -39358,7 +39590,7 @@ function planChanges(input) {
|
|
|
39358
39590
|
}
|
|
39359
39591
|
const mt = model.get(q);
|
|
39360
39592
|
for (const c of def.columns) {
|
|
39361
|
-
const bound =
|
|
39593
|
+
const bound = bindColumn(role, c);
|
|
39362
39594
|
const actual = mt.columns.get(c.name);
|
|
39363
39595
|
if (!actual) {
|
|
39364
39596
|
operations.push({ op: "add_column", table: q, column: bound });
|
|
@@ -39382,7 +39614,7 @@ function planChanges(input) {
|
|
|
39382
39614
|
if (!actual) {
|
|
39383
39615
|
operations.push(constraint(c));
|
|
39384
39616
|
t.changes.push(`add CHECK ${c.name}`);
|
|
39385
|
-
} else if (stable(actual.check) !== stable(bind(c.check)) || actual.hint !== c.hint) {
|
|
39617
|
+
} else if (stable(actual.check) !== stable(bind(binder.where(role, c.check))) || actual.hint !== c.hint) {
|
|
39386
39618
|
operations.push({ op: "drop_constraint", table: q, name: c.name }, constraint(c));
|
|
39387
39619
|
t.changes.push(`replace CHECK ${c.name}`);
|
|
39388
39620
|
}
|
|
@@ -39410,13 +39642,14 @@ function planChanges(input) {
|
|
|
39410
39642
|
for (const v of Object.keys(m.views)) {
|
|
39411
39643
|
const def = m.views[v];
|
|
39412
39644
|
const q = `${catalog}.${viewSchema}.${v}`;
|
|
39645
|
+
const query = binder.query(v, def.query);
|
|
39413
39646
|
const create = {
|
|
39414
39647
|
op: "create_view",
|
|
39415
39648
|
catalog,
|
|
39416
39649
|
schema: viewSchema,
|
|
39417
39650
|
name: v,
|
|
39418
39651
|
description: def.description,
|
|
39419
|
-
query: bind(
|
|
39652
|
+
query: bind(query),
|
|
39420
39653
|
...def.aliases !== void 0 ? { aliases: def.aliases } : {},
|
|
39421
39654
|
...def.labels !== void 0 ? { labels: def.labels } : {}
|
|
39422
39655
|
};
|
|
@@ -39425,7 +39658,7 @@ function planChanges(input) {
|
|
|
39425
39658
|
viewPlan.changes.push(`create view ${v}`);
|
|
39426
39659
|
} else if (action === "install") {
|
|
39427
39660
|
reconcileProblems.push(problem("view_exists", `${q} exists already`, "Drop or rename that view (drop_view), or install with --schema <another schema>.", EXT_DOCS.install, { table: q }));
|
|
39428
|
-
} else if (stable(oldManifest.views[v]) !== stable(def)) {
|
|
39661
|
+
} else if (stable(oldManifest.views[v]) !== stable(def) || stable(query) !== stable(def.query)) {
|
|
39429
39662
|
operations.push({ ...create, replace: true });
|
|
39430
39663
|
viewPlan.changes.push(`replace view ${v}`);
|
|
39431
39664
|
}
|
|
@@ -39441,6 +39674,17 @@ function planChanges(input) {
|
|
|
39441
39674
|
if (m.tables[role] === void 0) notes.push(`${role} (${rt.table}) is no longer used by the extension: it stays in the database`);
|
|
39442
39675
|
}
|
|
39443
39676
|
}
|
|
39677
|
+
for (const i of binder.issues) {
|
|
39678
|
+
reconcileProblems.push(
|
|
39679
|
+
problem(
|
|
39680
|
+
"column_map_unsupported",
|
|
39681
|
+
`${i.role}: ${i.where} names column ${i.column}, which reads your column ${binder.name(i.role, i.column)}: the installer cannot rewrite it there`,
|
|
39682
|
+
`Rename your column to ${i.column} with rename_column and leave --map ${i.role}.${i.column} out, or ask the extension's author.`,
|
|
39683
|
+
EXT_DOCS.tables,
|
|
39684
|
+
{ role: i.role, column: i.column }
|
|
39685
|
+
)
|
|
39686
|
+
);
|
|
39687
|
+
}
|
|
39444
39688
|
const toolProblems = [];
|
|
39445
39689
|
const others = input.others.filter((o) => o.name !== name);
|
|
39446
39690
|
const boundNames = (roles) => roles.map((r) => tables[r]);
|
|
@@ -39561,16 +39805,18 @@ function renderPlan(plan) {
|
|
|
39561
39805
|
lines.push("", plan.agent_message, plan.summary);
|
|
39562
39806
|
return lines.join("\n");
|
|
39563
39807
|
}
|
|
39564
|
-
var AGENT_MESSAGE, MAX_OPERATIONS,
|
|
39808
|
+
var AGENT_MESSAGE, FILTER_KEYS, MAX_OPERATIONS, isObject6, obj3, list2, tableName, isLink;
|
|
39565
39809
|
var init_extension_plan = __esm({
|
|
39566
39810
|
"src/api/extension-plan.ts"() {
|
|
39567
39811
|
"use strict";
|
|
39812
|
+
init_extension_columns();
|
|
39568
39813
|
init_extension_manifest();
|
|
39569
39814
|
init_guidance();
|
|
39570
39815
|
AGENT_MESSAGE = `For the agent installing this extension: show this plan to the user before anything is written. Where a table says "proposed", the installer chose it: ask the user to confirm it, to pick another table (--map <role>=<catalog.schema.table>, --map <role>.<column>=<column>), or to create it (--create <role>). Add a missing column only after the user agrees (--add <role>.<column>). Solve each problem the same way. Then run the same command again with the user's choices and --yes, only once the user approved the tables it creates or changes, the tools and the token's permissions.`;
|
|
39816
|
+
FILTER_KEYS = /* @__PURE__ */ new Set(["check", "where", "ref_where", "query", "on"]);
|
|
39571
39817
|
MAX_OPERATIONS = 200;
|
|
39572
|
-
|
|
39573
|
-
obj3 = (v) =>
|
|
39818
|
+
isObject6 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
39819
|
+
obj3 = (v) => isObject6(v) ? v : {};
|
|
39574
39820
|
list2 = (v) => Array.isArray(v) ? v : [];
|
|
39575
39821
|
tableName = (t, role) => t.name ?? role;
|
|
39576
39822
|
isLink = (c) => c.type === "link" || c.type === "list" && c.items === "link";
|
|
@@ -40697,7 +40943,7 @@ __export(util_exports, {
|
|
|
40697
40943
|
hexToUint8Array: () => hexToUint8Array,
|
|
40698
40944
|
hide: () => hide,
|
|
40699
40945
|
installLazyProp: () => installLazyProp,
|
|
40700
|
-
isObject: () =>
|
|
40946
|
+
isObject: () => isObject7,
|
|
40701
40947
|
isPlainObject: () => isPlainObject2,
|
|
40702
40948
|
issue: () => issue,
|
|
40703
40949
|
joinValues: () => joinValues,
|
|
@@ -40904,11 +41150,11 @@ function esc(str3) {
|
|
|
40904
41150
|
function slugify(input) {
|
|
40905
41151
|
return input.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
40906
41152
|
}
|
|
40907
|
-
function
|
|
41153
|
+
function isObject7(data) {
|
|
40908
41154
|
return typeof data === "object" && data !== null && !Array.isArray(data);
|
|
40909
41155
|
}
|
|
40910
41156
|
function isPlainObject2(o) {
|
|
40911
|
-
if (
|
|
41157
|
+
if (isObject7(o) === false)
|
|
40912
41158
|
return false;
|
|
40913
41159
|
const ctor = o.constructor;
|
|
40914
41160
|
if (ctor === void 0)
|
|
@@ -40916,7 +41162,7 @@ function isPlainObject2(o) {
|
|
|
40916
41162
|
if (typeof ctor !== "function")
|
|
40917
41163
|
return true;
|
|
40918
41164
|
const prot = ctor.prototype;
|
|
40919
|
-
if (
|
|
41165
|
+
if (isObject7(prot) === false)
|
|
40920
41166
|
return false;
|
|
40921
41167
|
if (Object.prototype.hasOwnProperty.call(prot, "isPrototypeOf") === false) {
|
|
40922
41168
|
return false;
|
|
@@ -43315,7 +43561,7 @@ var init_schemas = __esm({
|
|
|
43315
43561
|
}
|
|
43316
43562
|
return propValues;
|
|
43317
43563
|
});
|
|
43318
|
-
const
|
|
43564
|
+
const isObject8 = isObject7;
|
|
43319
43565
|
const catchall = def.catchall;
|
|
43320
43566
|
let value;
|
|
43321
43567
|
const memo2 = globalConfig.memoizer;
|
|
@@ -43323,7 +43569,7 @@ var init_schemas = __esm({
|
|
|
43323
43569
|
inst._zod.parse = (payload, ctx) => {
|
|
43324
43570
|
value ?? (value = _normalized.value);
|
|
43325
43571
|
const input = payload.value;
|
|
43326
|
-
if (!
|
|
43572
|
+
if (!isObject8(input)) {
|
|
43327
43573
|
payload.issues.push({
|
|
43328
43574
|
expected: "object",
|
|
43329
43575
|
code: "invalid_type",
|
|
@@ -43459,7 +43705,7 @@ var init_schemas = __esm({
|
|
|
43459
43705
|
return doc.compile();
|
|
43460
43706
|
};
|
|
43461
43707
|
let fastpass;
|
|
43462
|
-
const
|
|
43708
|
+
const isObject8 = isObject7;
|
|
43463
43709
|
const jit = !globalConfig.jitless;
|
|
43464
43710
|
const allowsEval2 = allowsEval;
|
|
43465
43711
|
const fastEnabled = jit && allowsEval2.value;
|
|
@@ -43468,7 +43714,7 @@ var init_schemas = __esm({
|
|
|
43468
43714
|
inst._zod.parse = (payload, ctx) => {
|
|
43469
43715
|
value ?? (value = _normalized.value);
|
|
43470
43716
|
const input = payload.value;
|
|
43471
|
-
if (!
|
|
43717
|
+
if (!isObject8(input)) {
|
|
43472
43718
|
payload.issues.push({
|
|
43473
43719
|
expected: "object",
|
|
43474
43720
|
code: "invalid_type",
|
|
@@ -43568,7 +43814,7 @@ var init_schemas = __esm({
|
|
|
43568
43814
|
const disc = cached(() => discriminatorMap(def));
|
|
43569
43815
|
inst._zod.parse = (payload, ctx) => {
|
|
43570
43816
|
const input = payload.value;
|
|
43571
|
-
if (!
|
|
43817
|
+
if (!isObject7(input)) {
|
|
43572
43818
|
payload.issues.push({
|
|
43573
43819
|
code: "invalid_type",
|
|
43574
43820
|
expected: "object",
|
|
@@ -77387,7 +77633,7 @@ function setUp(o, vault, home, explicit, self) {
|
|
|
77387
77633
|
// src/node/service.ts
|
|
77388
77634
|
init_local();
|
|
77389
77635
|
init_hints();
|
|
77390
|
-
var VERSION = true ? "3.4.
|
|
77636
|
+
var VERSION = true ? "3.4.2" : "dev";
|
|
77391
77637
|
function parsePort(raw, from = "--port") {
|
|
77392
77638
|
const n = /^\s*\d+\s*$/.test(raw) ? Number(raw) : NaN;
|
|
77393
77639
|
if (n === 0 || Number.isInteger(n) && n >= 1024 && n <= 65535) return n;
|
|
@@ -78452,7 +78698,7 @@ Docs: https://datanotes.dev`;
|
|
|
78452
78698
|
|
|
78453
78699
|
// src/node/cli.ts
|
|
78454
78700
|
init_message();
|
|
78455
|
-
var VERSION2 = true ? "3.4.
|
|
78701
|
+
var VERSION2 = true ? "3.4.2" : "dev";
|
|
78456
78702
|
var consoleError = console.error;
|
|
78457
78703
|
async function main5(argv = process.argv.slice(2)) {
|
|
78458
78704
|
const [command, ...rest] = argv;
|
|
@@ -89,7 +89,7 @@ Five options carry your choices. They apply to `install` and, except the last tw
|
|
|
89
89
|
| `--catalog` | Sets the catalog of created tables and views. | `--catalog work` |
|
|
90
90
|
| `--schema` | Sets the schema of created owned tables and views. | `--schema reading` |
|
|
91
91
|
|
|
92
|
-
Each option takes a comma-separated list. `--map role=catalog.schema.table` binds a table. `--map role.column=column` maps a column. A column of the extension’s own table keeps the extension’s name: you cannot map it.
|
|
92
|
+
Each option takes a comma-separated list. `--map role=catalog.schema.table` binds a table. `--map role.column=column` maps a column of a table the extension depends on. The views, CHECKs and link filters of the extension then read your column, through links too. A column of the extension’s own table keeps the extension’s name: you cannot map it.
|
|
93
93
|
|
|
94
94
|
The plan ends with a message for the AI agent that runs the install:
|
|
95
95
|
|
|
@@ -101,6 +101,7 @@ An agent never guesses. It shows the plan, asks the user, and runs the command a
|
|
|
101
101
|
* `ambiguous_dependency`: several tables could be the role. Pass `--map` or `--create`.
|
|
102
102
|
* `missing_column`: the matched table lacks a column. Map another column, or add it with `--add`.
|
|
103
103
|
* `column_differs`: a column exists with another type or rule. Map another column or table.
|
|
104
|
+
* `column_map_unsupported`: the extension names a mapped column where the installer cannot rewrite it, such as the outer clauses of a `union` view. Rename your column to the extension’s name and leave the mapping out.
|
|
104
105
|
* `table_exists`: a table the extension would create exists already. Use it with `--map`, or pick another catalog.
|
|
105
106
|
* `tool_name_taken`: a tool name is already an operation or a tool of another extension.
|
|
106
107
|
* `token_exists`: a token named `ext-<name>` exists already.
|
package/dist/docs/api.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Datanotes HTTP API
|
|
2
2
|
|
|
3
|
-
> OpenAPI 3.1.0 description of the Datanotes 3.4.
|
|
3
|
+
> OpenAPI 3.1.0 description of the Datanotes 3.4.2 HTTP API: https://datanotes.dev/openapi.json
|
|
4
4
|
|
|
5
5
|
Database of Markdown notes: tables of notes, rows, queries, history, a change feed and the vault's notes.
|
|
6
6
|
|