@intentius/chant-lexicon-sql 0.124.54 → 0.124.56

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.
Files changed (61) hide show
  1. package/dist/capability-plugin.d.ts +160 -0
  2. package/dist/capability-plugin.d.ts.map +1 -0
  3. package/dist/capability-plugin.js +370 -0
  4. package/dist/change-set.d.ts +35 -0
  5. package/dist/change-set.d.ts.map +1 -1
  6. package/dist/change-set.js +43 -0
  7. package/dist/ci-setup.d.ts +34 -0
  8. package/dist/ci-setup.d.ts.map +1 -0
  9. package/dist/ci-setup.js +106 -0
  10. package/dist/clickhouse/access.d.ts +20 -0
  11. package/dist/clickhouse/access.d.ts.map +1 -1
  12. package/dist/clickhouse/access.js +42 -1
  13. package/dist/clickhouse/live/describe-resources.d.ts +3 -1
  14. package/dist/clickhouse/live/describe-resources.d.ts.map +1 -1
  15. package/dist/clickhouse/live/describe-resources.js +31 -3
  16. package/dist/clickhouse/plan/deep.d.ts +42 -11
  17. package/dist/clickhouse/plan/deep.d.ts.map +1 -1
  18. package/dist/clickhouse/plan/deep.js +104 -33
  19. package/dist/exit-codes.d.ts +69 -0
  20. package/dist/exit-codes.d.ts.map +1 -0
  21. package/dist/exit-codes.js +105 -0
  22. package/dist/index.d.ts +4 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +12 -0
  25. package/dist/integrity.json +2 -2
  26. package/dist/lock-grain.d.ts +27 -0
  27. package/dist/lock-grain.d.ts.map +1 -0
  28. package/dist/lock-grain.js +13 -0
  29. package/dist/manifest.json +1 -1
  30. package/dist/plugin.d.ts.map +1 -1
  31. package/dist/plugin.js +8 -0
  32. package/dist/postgres/testing/migration.d.ts +2 -0
  33. package/dist/postgres/testing/migration.d.ts.map +1 -1
  34. package/dist/token-source.d.ts +28 -0
  35. package/dist/token-source.d.ts.map +1 -1
  36. package/dist/token-source.js +70 -1
  37. package/dist/yodel-run.d.ts +58 -0
  38. package/dist/yodel-run.d.ts.map +1 -0
  39. package/dist/yodel-run.js +145 -0
  40. package/package.json +2 -2
  41. package/src/capability-plugin.test.ts +317 -0
  42. package/src/capability-plugin.ts +515 -0
  43. package/src/change-set.ts +74 -0
  44. package/src/ci-setup.test.ts +67 -0
  45. package/src/ci-setup.ts +116 -0
  46. package/src/clickhouse/access.ts +49 -2
  47. package/src/clickhouse/live/access.e2e.test.ts +8 -7
  48. package/src/clickhouse/live/describe-resources.ts +27 -3
  49. package/src/clickhouse/plan/deep-grants-drift.test.ts +151 -0
  50. package/src/clickhouse/plan/deep-grants.test.ts +10 -5
  51. package/src/clickhouse/plan/deep.ts +103 -32
  52. package/src/exit-codes.test.ts +46 -0
  53. package/src/exit-codes.ts +140 -0
  54. package/src/index.ts +28 -0
  55. package/src/lock-grain.test.ts +57 -0
  56. package/src/lock-grain.ts +37 -0
  57. package/src/plugin.ts +9 -0
  58. package/src/testdata/fake-yodel.mjs +58 -0
  59. package/src/token-source-traded.test.ts +75 -0
  60. package/src/token-source.ts +74 -1
  61. package/src/yodel-run.ts +175 -0
@@ -0,0 +1,34 @@
1
+ import type { CiService } from "@intentius/chant/components/ci-services";
2
+ export type SqlCiDialect = "clickhouse" | "postgres";
3
+ /** The profile the replay check runs on, as sql-yodeler names it. */
4
+ export declare const REPLAY_PROFILE = "replay";
5
+ /** The oldest Node the lexicon and chant run on. */
6
+ export declare const SQL_CI_NODE_MAJOR = 22;
7
+ /** Fails the job, naming the version, when the image's Node is older than {@link SQL_CI_NODE_MAJOR}. */
8
+ export declare const NODE_CHECK_LINE: string;
9
+ /**
10
+ * The project's packages, unless the lexicon is already installed (an image
11
+ * that carries them pays nothing), and their bins on PATH: exported for the
12
+ * rest of a GitLab script, appended to `$GITHUB_PATH` for a GitHub or
13
+ * Forgejo job's later steps.
14
+ */
15
+ export declare const PACKAGES_INSTALL_LINE = "{ [ -d node_modules/@intentius/chant-lexicon-sql ] || npm ci; } && export PATH=\"$PWD/node_modules/.bin:$PATH\" && if [ -n \"${GITHUB_PATH:-}\" ]; then echo \"$PWD/node_modules/.bin\" >> \"$GITHUB_PATH\"; fi";
16
+ /** The replay server of each dialect: the emulator's pinned image, under a prefixed name. */
17
+ export declare function replayService(dialect: SqlCiDialect): CiService;
18
+ /**
19
+ * The dialects the config names (`dialect: "postgres"`, or a list), in
20
+ * {@link SqlCiDialect} order; ClickHouse, the lexicon's default, when it
21
+ * names none.
22
+ */
23
+ export declare function sqlDialectsOf(text: string): SqlCiDialect[];
24
+ /** Whether the config declares the `replay` profile, the one the replay check runs on. */
25
+ export declare function declaresReplay(text: string): boolean;
26
+ /** The setup lines for the project at `dir`: none when it has no config naming the sql lexicon's settings. */
27
+ export declare function sqlCiSetup(ctx: {
28
+ dir: string;
29
+ }): string[];
30
+ /** The replay service per dialect for the project at `dir`, when its config declares the `replay` profile. */
31
+ export declare function sqlCiServices(ctx: {
32
+ dir: string;
33
+ }): CiService[];
34
+ //# sourceMappingURL=ci-setup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ci-setup.d.ts","sourceRoot":"","sources":["../src/ci-setup.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIzE,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,UAAU,CAAC;AAErD,qEAAqE;AACrE,eAAO,MAAM,cAAc,WAAW,CAAC;AAEvC,oDAAoD;AACpD,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC,wGAAwG;AACxG,eAAO,MAAM,eAAe,QAA4P,CAAC;AAEzR;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,oNACyK,CAAC;AAE5M,6FAA6F;AAC7F,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,SAAS,CAqB9D;AAgBD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,EAAE,CAM1D;AAED,0FAA0F;AAC1F,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED,8GAA8G;AAC9G,wBAAgB,UAAU,CAAC,GAAG,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,EAAE,CAEzD;AAED,8GAA8G;AAC9G,wBAAgB,aAAa,CAAC,GAAG,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EAAE,CAI/D"}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * What a SQL project's CI jobs need from the sql lexicon (#4572, over
3
+ * #4550's `LexiconPlugin.ciSetup`): the setup lines for chant's default
4
+ * image, and the replay server per dialect as a job service
5
+ * (`LexiconPlugin.ciServices`). sql-yodeler's own versions were
6
+ * `src/ci/render.ts` (`installLines`, `REPLAY_SERVICE`, `waitFor`), which
7
+ * now come from here.
8
+ *
9
+ * The lines: a Node check, since the lexicon and chant run on Node 22, then
10
+ * the project's packages with `npm ci` unless the lexicon is already
11
+ * installed, with `node_modules/.bin` put on PATH for the job's later
12
+ * steps. That one install carries the dialect's client too: Postgres is the
13
+ * lexicon's own `pg` dependency and ClickHouse is spoken over HTTP, so no
14
+ * separate client goes into the image. A token source needs no cloud
15
+ * command line in CI either: in a job that traded its OIDC token through
16
+ * chant's `ci.identity`, `./token-source.ts` mints from what the trade left.
17
+ *
18
+ * The replay server is the image the lexicon's emulator runs, at the same
19
+ * pin (`./spec/pin.ts`, `./spec/postgres-pin.ts`), so a replay in CI and
20
+ * `chant emulator up` on a laptop run one server. It is a throwaway: the
21
+ * replay resets it. The dialects are read from the text of the project's
22
+ * `chant.config.ts`, never by running it, as lexicon detection reads source.
23
+ */
24
+ import { existsSync, readFileSync } from "node:fs";
25
+ import { join } from "node:path";
26
+ import { clickhouseImage } from "./spec/pin.js";
27
+ import { POSTGRES_LATEST_MAJOR, postgresImage } from "./spec/postgres-pin.js";
28
+ /** The profile the replay check runs on, as sql-yodeler names it. */
29
+ export const REPLAY_PROFILE = "replay";
30
+ /** The oldest Node the lexicon and chant run on. */
31
+ export const SQL_CI_NODE_MAJOR = 22;
32
+ /** Fails the job, naming the version, when the image's Node is older than {@link SQL_CI_NODE_MAJOR}. */
33
+ export const NODE_CHECK_LINE = `node -e 'const m = Number(process.versions.node.split(".")[0]); if (m < ${String(SQL_CI_NODE_MAJOR)}) { console.error("the sql lexicon needs Node ${String(SQL_CI_NODE_MAJOR)} or newer; this image has " + process.versions.node); process.exit(1); }'`;
34
+ /**
35
+ * The project's packages, unless the lexicon is already installed (an image
36
+ * that carries them pays nothing), and their bins on PATH: exported for the
37
+ * rest of a GitLab script, appended to `$GITHUB_PATH` for a GitHub or
38
+ * Forgejo job's later steps.
39
+ */
40
+ export const PACKAGES_INSTALL_LINE = '{ [ -d node_modules/@intentius/chant-lexicon-sql ] || npm ci; } && export PATH="$PWD/node_modules/.bin:$PATH" && if [ -n "${GITHUB_PATH:-}" ]; then echo "$PWD/node_modules/.bin" >> "$GITHUB_PATH"; fi';
41
+ /** The replay server of each dialect: the emulator's pinned image, under a prefixed name. */
42
+ export function replayService(dialect) {
43
+ if (dialect === "clickhouse") {
44
+ return {
45
+ name: "replay-clickhouse",
46
+ image: clickhouseImage(),
47
+ port: 8123,
48
+ // The default user, with no password, from outside the container.
49
+ env: { CLICKHOUSE_SKIP_USER_SETUP: "1" },
50
+ health: "clickhouse-client --query 'SELECT 1'",
51
+ jobEnv: { REPLAY_CLICKHOUSE_URL: "http://replay-clickhouse:8123" },
52
+ };
53
+ }
54
+ return {
55
+ name: "replay-postgres",
56
+ image: postgresImage(POSTGRES_LATEST_MAJOR),
57
+ port: 5432,
58
+ env: { POSTGRES_PASSWORD: "replay" },
59
+ // Over TCP: during its first start the image runs a server on the socket only, then restarts.
60
+ health: "pg_isready -h 127.0.0.1 -U postgres",
61
+ jobEnv: { REPLAY_POSTGRES_URL: "postgres://replay-postgres:5432/postgres", REPLAY_POSTGRES_USER: "postgres", REPLAY_POSTGRES_PASSWORD: "replay" },
62
+ };
63
+ }
64
+ /** The text of the project's config, or undefined without one. */
65
+ function configText(dir) {
66
+ for (const name of ["chant.config.ts", "chant.config.mts", "chant.config.js"]) {
67
+ const file = join(dir, name);
68
+ if (!existsSync(file))
69
+ continue;
70
+ try {
71
+ return readFileSync(file, "utf-8");
72
+ }
73
+ catch {
74
+ return undefined;
75
+ }
76
+ }
77
+ return undefined;
78
+ }
79
+ /**
80
+ * The dialects the config names (`dialect: "postgres"`, or a list), in
81
+ * {@link SqlCiDialect} order; ClickHouse, the lexicon's default, when it
82
+ * names none.
83
+ */
84
+ export function sqlDialectsOf(text) {
85
+ const m = /\bdialect\s*:\s*(\[[^\]]*\]|["'][a-z]+["'])/.exec(text);
86
+ if (!m)
87
+ return ["clickhouse"];
88
+ const named = new Set([...m[1].matchAll(/["']([a-z]+)["']/g)].map((x) => x[1]));
89
+ const out = ["clickhouse", "postgres"].filter((d) => named.has(d));
90
+ return out.length > 0 ? [...out] : ["clickhouse"];
91
+ }
92
+ /** Whether the config declares the `replay` profile, the one the replay check runs on. */
93
+ export function declaresReplay(text) {
94
+ return new RegExp(`(^|[\\s{,])["']?${REPLAY_PROFILE}["']?\\s*:`).test(text);
95
+ }
96
+ /** The setup lines for the project at `dir`: none when it has no config naming the sql lexicon's settings. */
97
+ export function sqlCiSetup(ctx) {
98
+ return configText(ctx.dir) === undefined ? [] : [NODE_CHECK_LINE, PACKAGES_INSTALL_LINE];
99
+ }
100
+ /** The replay service per dialect for the project at `dir`, when its config declares the `replay` profile. */
101
+ export function sqlCiServices(ctx) {
102
+ const text = configText(ctx.dir);
103
+ if (text === undefined || !declaresReplay(text))
104
+ return [];
105
+ return sqlDialectsOf(text).map(replayService);
106
+ }
@@ -116,6 +116,26 @@ export declare function atomKey(a: GrantAtom): string;
116
116
  export declare function foldAtoms(atoms: readonly GrantAtom[]): GrantAtom[];
117
117
  /** A grantee's grants as a plan compares them: each atom's key, mapped to the atom. */
118
118
  export declare function grantsCanonical(ddl: string, grantee: string, defaultDatabase?: string): Record<string, string>;
119
+ /**
120
+ * The atoms one grant declaration gives, one per grantee (#4582): `key` is
121
+ * the atom as the declaration spells its target (a bare table stays bare),
122
+ * which is how the deep diff addresses it on both sides; `atom` has the
123
+ * target qualified with `defaultDatabase`, which is what the server is asked
124
+ * about.
125
+ */
126
+ export declare function declaredGrantAtoms(ddl: string, defaultDatabase?: string): Array<{
127
+ grantee: string;
128
+ key: string;
129
+ atom: GrantAtom;
130
+ }>;
131
+ /**
132
+ * Whether a grantee holding `have` (its `SHOW GRANTS` atoms) holds `a`: the
133
+ * same atom, or one the server folds it under (the table's privilege for a
134
+ * column's, the database's for a table's, `*.*` for any), with a grant or
135
+ * admin option at least as wide, and no partial revoke the server holds
136
+ * taking part of it back.
137
+ */
138
+ export declare function holdsAtom(have: readonly GrantAtom[], a: GrantAtom): boolean;
119
139
  /** Every grantee a grant statement names. */
120
140
  export declare function granteesOf(ddl: string): string[];
121
141
  /** The statement that gives `grantee` one atom. */
@@ -1 +1 @@
1
- {"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../../src/clickhouse/access.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../../src/clickhouse/access.ts"],"names":[],"mappings":"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"}
@@ -520,6 +520,7 @@ export function atomKey(a) {
520
520
  return `ROLE ${a.role}${a.option ? " WITH ADMIN OPTION" : ""}`;
521
521
  return `${a.revoke ? "REVOKE " : ""}${a.privilege}${a.column !== undefined ? `(${a.column})` : ""} ON ${a.target}${a.option ? " WITH GRANT OPTION" : ""}`;
522
522
  }
523
+ /** Each statement's grantees and atoms. A bare table is qualified with `defaultDatabase`, or left bare when it is undefined. */
523
524
  function grantAtoms(ddl, defaultDatabase) {
524
525
  const out = [];
525
526
  for (const statement of splitLines(ddl)) {
@@ -534,7 +535,7 @@ function grantAtoms(ddl, defaultDatabase) {
534
535
  const parts = sigOf(tokens, node.target)
535
536
  .filter((t) => !(t.kind === "punct" && t.text === "."))
536
537
  .map((t) => unquote(t.text));
537
- target = parts.length >= 2 ? `${parts[0]}.${parts[1]}` : parts[0] === "*" ? "*" : `${defaultDatabase}.${parts[0]}`;
538
+ target = parts.length >= 2 ? `${parts[0]}.${parts[1]}` : parts[0] === "*" || defaultDatabase === undefined ? parts[0] : `${defaultDatabase}.${parts[0]}`;
538
539
  }
539
540
  for (const item of node.items) {
540
541
  const sig = sigOf(tokens, item);
@@ -603,6 +604,46 @@ export function grantsCanonical(ddl, grantee, defaultDatabase = "default") {
603
604
  .map((a) => [atomKey(a), JSON.stringify(a)])
604
605
  .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
605
606
  }
607
+ /**
608
+ * The atoms one grant declaration gives, one per grantee (#4582): `key` is
609
+ * the atom as the declaration spells its target (a bare table stays bare),
610
+ * which is how the deep diff addresses it on both sides; `atom` has the
611
+ * target qualified with `defaultDatabase`, which is what the server is asked
612
+ * about.
613
+ */
614
+ export function declaredGrantAtoms(ddl, defaultDatabase = "default") {
615
+ const spelled = grantAtoms(ddl, undefined);
616
+ const resolved = grantAtoms(ddl, defaultDatabase);
617
+ const out = [];
618
+ spelled.forEach((g, i) => {
619
+ for (const grantee of g.grantees) {
620
+ g.atoms.forEach((a, j) => out.push({ grantee, key: atomKey(a), atom: resolved[i].atoms[j] }));
621
+ }
622
+ });
623
+ return out;
624
+ }
625
+ /**
626
+ * Whether a grantee holding `have` (its `SHOW GRANTS` atoms) holds `a`: the
627
+ * same atom, or one the server folds it under (the table's privilege for a
628
+ * column's, the database's for a table's, `*.*` for any), with a grant or
629
+ * admin option at least as wide, and no partial revoke the server holds
630
+ * taking part of it back.
631
+ */
632
+ export function holdsAtom(have, a) {
633
+ if (a.role !== undefined)
634
+ return have.some((b) => b.role === a.role && (b.option ?? false) >= (a.option ?? false));
635
+ const granted = have.some((b) => !b.revoke &&
636
+ b.role === undefined &&
637
+ b.privilege === a.privilege &&
638
+ (b.option ?? false) >= (a.option ?? false) &&
639
+ ((b.target === a.target && (b.column === undefined || b.column === a.column)) || (b.column === undefined && covers(b.target, a.target))));
640
+ if (!granted)
641
+ return false;
642
+ return !have.some((r) => r.revoke === true &&
643
+ r.privilege === a.privilege &&
644
+ (r.target === a.target || (a.column === undefined && covers(a.target, r.target))) &&
645
+ (a.column === undefined || r.column === undefined || r.column === a.column));
646
+ }
606
647
  /** Every grantee a grant statement names. */
607
648
  export function granteesOf(ddl) {
608
649
  return [...new Set(grantAtoms(ddl, "default").flatMap((g) => g.grantees))];
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * `describeResources()` for the ClickHouse dialect: which declared databases,
3
- * tables and views exist on the environment's server.
3
+ * tables and views exist on the environment's server. A grant declaration is
4
+ * present when the server has every grantee it names, and missing when it
5
+ * lacks one (#4582); which privileges the grantee holds is the deep diff's.
4
6
  *
5
7
  * One read of `system.databases`, `system.tables` and `system.functions` per
6
8
  * run, then a lookup per declared entity, so N entities are three queries,
@@ -1 +1 @@
1
- {"version":3,"file":"describe-resources.d.ts","sourceRoot":"","sources":["../../../src/clickhouse/live/describe-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,uBAAuB,EAG7B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAA6C,KAAK,WAAW,EAAyB,MAAM,QAAQ,CAAC;AAY5G,6DAA6D;AAC7D,wBAAgB,eAAe,CAAC,MAAM,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,GAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAapH;AAkDD,wBAAsB,iBAAiB,CACrC,OAAO,EAAE;IACP,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC9E,mEAAmE;IACnE,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GAAG,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,GACnC,OAAO,CAAC,uBAAuB,CAAC,CAMlC"}
1
+ {"version":3,"file":"describe-resources.d.ts","sourceRoot":"","sources":["../../../src/clickhouse/live/describe-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,uBAAuB,EAG7B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAA6C,KAAK,WAAW,EAAyB,MAAM,QAAQ,CAAC;AAa5G,6DAA6D;AAC7D,wBAAgB,eAAe,CAAC,MAAM,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,GAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAapH;AA6DD,wBAAsB,iBAAiB,CACrC,OAAO,EAAE;IACP,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC9E,mEAAmE;IACnE,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GAAG,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,GACnC,OAAO,CAAC,uBAAuB,CAAC,CAgBlC"}
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * `describeResources()` for the ClickHouse dialect: which declared databases,
3
- * tables and views exist on the environment's server.
3
+ * tables and views exist on the environment's server. A grant declaration is
4
+ * present when the server has every grantee it names, and missing when it
5
+ * lacks one (#4582); which privileges the grantee holds is the deep diff's.
4
6
  *
5
7
  * One read of `system.databases`, `system.tables` and `system.functions` per
6
8
  * run, then a lookup per declared entity, so N entities are three queries,
@@ -19,6 +21,7 @@ import { observeEntities, } from "@intentius/chant/observation";
19
21
  import { bindClickHouse, classifyClickHouseFailure } from "./bind.js";
20
22
  import { ACCESS_ENTITY_TYPES, ACCESS_UNMANAGED_DETAIL, accessOf, readLiveAccess, readLiveSchema } from "./catalog.js";
21
23
  import { CLICKHOUSE_ENTITY_TYPES } from "../entities.js";
24
+ import { granteesOf } from "../access.js";
22
25
  import { objectKey } from "../plan/normalize.js";
23
26
  import { isChantManaged, readMarker, stripMarker } from "../ownership.js";
24
27
  /** The database and name a declared entity is created as. */
@@ -54,7 +57,21 @@ function adapter(options) {
54
57
  return { unobserved: { reason: "filtered", detail: ACCESS_UNMANAGED_DETAIL } };
55
58
  }
56
59
  if (entity.type === CLICKHOUSE_ENTITY_TYPES.grant) {
57
- return { unobserved: { reason: "unsupported-kind", detail: "a grant is compared as part of its grantees' grants, which chant sql plan reports" } };
60
+ let grantees;
61
+ try {
62
+ grantees = granteesOf(String(entity.props.ddl ?? ""));
63
+ }
64
+ catch (err) {
65
+ return { unobserved: { reason: "read-failed", detail: `the grant does not parse: ${err.message}` } };
66
+ }
67
+ const gone = grantees.find((g) => !byKey.has(objectKey({ type: entity.type, name: g })));
68
+ const queried = `${target.endpoint.url} grants ${gone ?? grantees.join(", ")}`;
69
+ if (gone !== undefined || grantees.length === 0)
70
+ return { absent: true, queried };
71
+ // A grant carries no comment, so no marker: like a user or a role, it is never chant's by marker.
72
+ if (options.owned)
73
+ return { unobserved: { reason: "filtered", detail: "a grant carries no chant ownership marker and owned was requested" }, queried };
74
+ return { present: { type: entity.type, physicalId: `grants ${grantees.join(", ")}`, status: "", ownership: "foreign" }, queried };
58
75
  }
59
76
  const { database, name } = declaredAddress(entity, target.defaultDatabase);
60
77
  const queried = `${target.endpoint.url} ${database ? `${database}.` : ""}${name}`;
@@ -87,5 +104,16 @@ export async function describeResources(options) {
87
104
  const entity = options.entities.get(name);
88
105
  return { name, type: entity?.entityType ?? "", props: entity?.props ?? {} };
89
106
  });
90
- return observeEntities(declared, adapter({ ...options, access: (db) => accessOf(options.entities, db) }));
107
+ const grantees = [...options.entities.values()]
108
+ .filter((e) => e.entityType === CLICKHOUSE_ENTITY_TYPES.grant)
109
+ .flatMap((e) => {
110
+ try {
111
+ return granteesOf(String(e.props.ddl ?? ""));
112
+ }
113
+ catch {
114
+ return [];
115
+ }
116
+ });
117
+ const access = (db) => [...accessOf(options.entities, db), ...[...new Set(grantees)].map((name) => ({ kind: "grants", name }))];
118
+ return observeEntities(declared, adapter({ ...options, access }));
91
119
  }
@@ -18,15 +18,35 @@
18
18
  * server's, that copy is what is reported, with the server in `observedOn`,
19
19
  * which the drift report prints as `seen on`.
20
20
  *
21
- * Where the profile manages access, the grants an apply would give or take
22
- * from each declared grantee are reported as `pending` (#3733), the same
23
- * statements `chant sql plan` plans (SQLCH273, SQLCH274), so a privilege
24
- * granted or revoked by hand after an approval moves the plan digest.
21
+ * Where the profile manages access, each grant declaration is read as
22
+ * `grants.<grantee>.<atom>` (#4582): a privilege or role it declares that the
23
+ * grantee does not hold (revoked by hand, SQLCH273) is `absent` drift on that
24
+ * declaration. A grant declaration whose grantee the server does not have is
25
+ * not read here; the thin diff reports it missing. A privilege a grantee holds
26
+ * that no declaration gives (granted by hand, SQLCH274) belongs to no one
27
+ * declaration, so it is reported as `pending` (#3733) with every statement an
28
+ * apply would send that grantee, as `chant sql plan` plans them. Either way a
29
+ * privilege granted or revoked by hand after an approval moves the plan digest.
25
30
  */
26
31
  import { type DeepObservationResult, type DeepPendingChange, type DeepNormalizationHooks } from "@intentius/chant/deep-observation";
27
32
  import { type BindOptions, type ClickHouseTarget } from "../live/bind.js";
28
33
  import { type CanonicalObject } from "./normalize.js";
34
+ import { type GrantAtom } from "../access.js";
29
35
  type Props = Record<string, unknown>;
36
+ /**
37
+ * A grant declaration's tree as the diff compares it (#4582):
38
+ * `{ grants: { <grantee>: { <atom>: "granted" } } }`, each atom as the
39
+ * declaration spells it. The reader writes the live side in the same shape,
40
+ * with the atoms the grantee holds. Undefined when the statement does not
41
+ * parse as a grant.
42
+ */
43
+ export declare function declaredGrantTree(ddl: string): Props | undefined;
44
+ /**
45
+ * A grant declaration's live tree (#4582): each atom it declares that the
46
+ * grantee holds, keyed as {@link declaredGrantTree} keys it. Undefined when
47
+ * the server does not have one of its grantees.
48
+ */
49
+ export declare function liveGrantTree(ddl: string, held: ReadonlyMap<string, readonly GrantAtom[]>, defaultDatabase: string): Props | undefined;
30
50
  export declare const sqlDeepNormalizationHooks: DeepNormalizationHooks;
31
51
  /** The live props, with each field the declaration means the same as written the declaration's way. */
32
52
  export declare function inDeclaredVocabulary(declared: Props, live: Props, d: CanonicalObject, l: CanonicalObject): Props;
@@ -39,16 +59,27 @@ export declare function observeResourcesDeep(options: {
39
59
  }>;
40
60
  } & Omit<BindOptions, "environment">): Promise<DeepObservationResult>;
41
61
  /**
42
- * The grants an apply would give or take from each declared grantee (#3733),
43
- * as `chant sql plan` compares them: the declared grants against what
44
- * `SHOW GRANTS FOR` prints, atom by atom. One change per grantee, its
45
- * REVOKEs before its GRANTs as the applier sends them, with the grant
46
- * declarations behind it. A grantee the server does not hold yet is given
47
- * every declared grant.
62
+ * What the server holds for each declared grantee, as `SHOW GRANTS FOR`
63
+ * prints it, folded into atoms. A grantee the server does not have is left
64
+ * out.
65
+ */
66
+ export declare function heldGrants(target: ClickHouseTarget, entities: ReadonlyMap<string, {
67
+ entityType: string;
68
+ props: Record<string, unknown>;
69
+ }>): Promise<Map<string, GrantAtom[]>>;
70
+ /**
71
+ * The grantees holding a privilege or role no declaration gives them
72
+ * (#3733), as `chant sql plan` compares them: the declared grants against
73
+ * what `SHOW GRANTS FOR` prints, atom by atom. One change per such grantee,
74
+ * every statement an apply would send it, its REVOKEs before its GRANTs as
75
+ * the applier sends them, with the grant declarations behind it. A grantee
76
+ * that only lacks declared grants has nothing pending: each declaration
77
+ * reports those as `absent` drift of its own (#4582). A grantee the server
78
+ * does not have is the thin diff's missing.
48
79
  */
49
80
  export declare function pendingGrants(target: ClickHouseTarget, entities: ReadonlyMap<string, {
50
81
  entityType: string;
51
82
  props: Record<string, unknown>;
52
- }>): Promise<DeepPendingChange[]>;
83
+ }>, held?: ReadonlyMap<string, readonly GrantAtom[]>): Promise<DeepPendingChange[]>;
53
84
  export {};
54
85
  //# sourceMappingURL=deep.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deep.d.ts","sourceRoot":"","sources":["../../../src/clickhouse/plan/deep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAEL,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EAEtB,KAAK,sBAAsB,EAC5B,MAAM,mCAAmC,CAAC;AAE3C,OAAO,EAA6C,KAAK,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMlH,OAAO,EAAkE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAInH,KAAK,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAKrC,eAAO,MAAM,yBAAyB,EAAE,sBAEvC,CAAC;AAgCF,uGAAuG;AACvG,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,eAAe,EAAE,CAAC,EAAE,eAAe,GAAG,KAAK,CAmDhH;AA4FD,wBAAsB,oBAAoB,CACxC,OAAO,EAAE;IACP,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;CAC/E,GAAG,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,GACnC,OAAO,CAAC,qBAAqB,CAAC,CAkGhC;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,gBAAgB,EACxB,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,GACpF,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAsB9B"}
1
+ {"version":3,"file":"deep.d.ts","sourceRoot":"","sources":["../../../src/clickhouse/plan/deep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAEL,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EAEtB,KAAK,sBAAsB,EAC5B,MAAM,mCAAmC,CAAC;AAE3C,OAAO,EAA6C,KAAK,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMlH,OAAO,EAAkE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AACnH,OAAO,EAA4F,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAGrI,KAAK,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAQrC;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS,CAQhE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC,EAAE,eAAe,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS,CAQtI;AAED,eAAO,MAAM,yBAAyB,EAAE,sBAOvC,CAAC;AAgCF,uGAAuG;AACvG,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,eAAe,EAAE,CAAC,EAAE,eAAe,GAAG,KAAK,CAmDhH;AA4FD,wBAAsB,oBAAoB,CACxC,OAAO,EAAE;IACP,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;CAC/E,GAAG,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,GACnC,OAAO,CAAC,qBAAqB,CAAC,CAuGhC;AAKD;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,CASnL;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,gBAAgB,EACxB,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,EACrF,IAAI,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC,GAC/C,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAqB9B"}
@@ -18,10 +18,15 @@
18
18
  * server's, that copy is what is reported, with the server in `observedOn`,
19
19
  * which the drift report prints as `seen on`.
20
20
  *
21
- * Where the profile manages access, the grants an apply would give or take
22
- * from each declared grantee are reported as `pending` (#3733), the same
23
- * statements `chant sql plan` plans (SQLCH273, SQLCH274), so a privilege
24
- * granted or revoked by hand after an approval moves the plan digest.
21
+ * Where the profile manages access, each grant declaration is read as
22
+ * `grants.<grantee>.<atom>` (#4582): a privilege or role it declares that the
23
+ * grantee does not hold (revoked by hand, SQLCH273) is `absent` drift on that
24
+ * declaration. A grant declaration whose grantee the server does not have is
25
+ * not read here; the thin diff reports it missing. A privilege a grantee holds
26
+ * that no declaration gives (granted by hand, SQLCH274) belongs to no one
27
+ * declaration, so it is reported as `pending` (#3733) with every statement an
28
+ * apply would send that grantee, as `chant sql plan` plans them. Either way a
29
+ * privilege granted or revoked by hand after an approval moves the plan digest.
25
30
  */
26
31
  import { deepObservation, } from "@intentius/chant/deep-observation";
27
32
  import { bindClickHouse, classifyClickHouseFailure } from "../live/bind.js";
@@ -31,12 +36,54 @@ import { sqlString } from "../apply/statements.js";
31
36
  import { stripMarkerFromStatement } from "../ownership.js";
32
37
  import { database, dictionary, func, policy, quota, role, settingsProfile, table, user, view, CLICKHOUSE_ENTITY_TYPES } from "../entities.js";
33
38
  import { canonicalObject, grantsObject, objectKey } from "./normalize.js";
34
- import { grantStatement, grantsByGrantee, revokeStatement } from "../access.js";
39
+ import { atomKey, declaredGrantAtoms, grantStatement, grantsByGrantee, holdsAtom, revokeStatement } from "../access.js";
35
40
  import { renderFor } from "../topology.js";
36
41
  // `concurrently` is how a Postgres index is created, not a property of it.
37
42
  const PRUNED = new Set(["ddl", "source", "lineage", "reads", "to", "orReplace", "ifNotExists", "concurrently"]);
43
+ /** What a held grant atom reads as, on both sides of the diff. */
44
+ const GRANTED = "granted";
45
+ /**
46
+ * A grant declaration's tree as the diff compares it (#4582):
47
+ * `{ grants: { <grantee>: { <atom>: "granted" } } }`, each atom as the
48
+ * declaration spells it. The reader writes the live side in the same shape,
49
+ * with the atoms the grantee holds. Undefined when the statement does not
50
+ * parse as a grant.
51
+ */
52
+ export function declaredGrantTree(ddl) {
53
+ try {
54
+ const grants = {};
55
+ for (const { grantee, key } of declaredGrantAtoms(ddl))
56
+ (grants[grantee] ?? (grants[grantee] = {}))[key] = GRANTED;
57
+ return { grants };
58
+ }
59
+ catch {
60
+ return undefined;
61
+ }
62
+ }
63
+ /**
64
+ * A grant declaration's live tree (#4582): each atom it declares that the
65
+ * grantee holds, keyed as {@link declaredGrantTree} keys it. Undefined when
66
+ * the server does not have one of its grantees.
67
+ */
68
+ export function liveGrantTree(ddl, held, defaultDatabase) {
69
+ const grants = {};
70
+ for (const { grantee, key, atom } of declaredGrantAtoms(ddl, defaultDatabase)) {
71
+ const have = held.get(grantee);
72
+ if (!have)
73
+ return undefined;
74
+ if (holdsAtom(have, atom))
75
+ (grants[grantee] ?? (grants[grantee] = {}))[key] = GRANTED;
76
+ }
77
+ return Object.keys(grants).length > 0 ? { grants } : {};
78
+ }
38
79
  export const sqlDeepNormalizationHooks = {
39
80
  prune: (node) => PRUNED.has(node.pattern.split(/[.[]/)[0]),
81
+ // A grant declaration is compared atom by atom (#4582); the reader writes the live side in this shape.
82
+ prepare: (tree, { entityType, side }) => {
83
+ if (entityType !== CLICKHOUSE_ENTITY_TYPES.grant || side !== "declared" || typeof tree.ddl !== "string")
84
+ return tree;
85
+ return declaredGrantTree(tree.ddl) ?? tree;
86
+ },
40
87
  };
41
88
  function liveProps(o) {
42
89
  const tag = o.type === CLICKHOUSE_ENTITY_TYPES.database
@@ -235,9 +282,11 @@ export async function observeResourcesDeep(options) {
235
282
  return deepObservation({}, unobserved);
236
283
  }
237
284
  const pending = [];
285
+ let held;
238
286
  if (target.access === true) {
239
287
  try {
240
- pending.push(...(await pendingGrants(target, options.entities)));
288
+ held = await heldGrants(target, options.entities);
289
+ pending.push(...(await pendingGrants(target, options.entities, held)));
241
290
  }
242
291
  catch (err) {
243
292
  const why = classifyClickHouseFailure(err);
@@ -259,13 +308,18 @@ export async function observeResourcesDeep(options) {
259
308
  continue;
260
309
  }
261
310
  if (entity.entityType === CLICKHOUSE_ENTITY_TYPES.grant) {
262
- if (unobserved[name])
311
+ if (unobserved[name] || !held)
263
312
  continue;
264
- unobserved[name] = {
265
- type: entity.entityType,
266
- reason: "unsupported-kind",
267
- detail: "a grant is compared as part of its grantees' grants: what an apply would grant or revoke is listed as pending (SQLCH273, SQLCH274)",
268
- };
313
+ const ddl = String(entity.props.ddl ?? "");
314
+ try {
315
+ const properties = liveGrantTree(ddl, held, target.defaultDatabase);
316
+ // A grantee the server does not have: the thin diff reports the declaration missing.
317
+ if (properties)
318
+ resources[name] = { type: entity.entityType, physicalId: `grants ${[...new Set(declaredGrantAtoms(ddl).map((a) => a.grantee))].join(", ")}`, properties };
319
+ }
320
+ catch (err) {
321
+ unobserved[name] = { type: entity.entityType, reason: "read-failed", detail: `the grant does not parse: ${err.message}` };
322
+ }
269
323
  continue;
270
324
  }
271
325
  const props = entity.props;
@@ -310,36 +364,53 @@ export async function observeResourcesDeep(options) {
310
364
  }
311
365
  return deepObservation(resources, unobserved, pending);
312
366
  }
367
+ const grantDeclarations = (entities) => grantsByGrantee([...entities].map(([name, e]) => ({ export: name, type: e.entityType, ddl: String(e.props.ddl ?? "") })));
313
368
  /**
314
- * The grants an apply would give or take from each declared grantee (#3733),
315
- * as `chant sql plan` compares them: the declared grants against what
316
- * `SHOW GRANTS FOR` prints, atom by atom. One change per grantee, its
317
- * REVOKEs before its GRANTs as the applier sends them, with the grant
318
- * declarations behind it. A grantee the server does not hold yet is given
319
- * every declared grant.
369
+ * What the server holds for each declared grantee, as `SHOW GRANTS FOR`
370
+ * prints it, folded into atoms. A grantee the server does not have is left
371
+ * out.
320
372
  */
321
- export async function pendingGrants(target, entities) {
322
- const declared = grantsByGrantee([...entities].map(([name, e]) => ({ export: name, type: e.entityType, ddl: String(e.props.ddl ?? "") })));
373
+ export async function heldGrants(target, entities) {
374
+ const declared = grantDeclarations(entities);
323
375
  if (declared.length === 0)
324
- return [];
376
+ return new Map();
325
377
  const live = await readLiveAccess(target, declared.map((g) => ({ kind: "grants", name: g.grantee })));
326
- const held = new Map(live.filter((o) => o.type === CLICKHOUSE_ENTITY_TYPES.grant).map((o) => [o.name, grantsObject(o.statement, o.name, target.defaultDatabase).access ?? {}]));
378
+ return new Map(live
379
+ .filter((o) => o.type === CLICKHOUSE_ENTITY_TYPES.grant)
380
+ .map((o) => [o.name, Object.values(grantsObject(o.statement, o.name, target.defaultDatabase).access ?? {}).map((json) => JSON.parse(json))]));
381
+ }
382
+ /**
383
+ * The grantees holding a privilege or role no declaration gives them
384
+ * (#3733), as `chant sql plan` compares them: the declared grants against
385
+ * what `SHOW GRANTS FOR` prints, atom by atom. One change per such grantee,
386
+ * every statement an apply would send it, its REVOKEs before its GRANTs as
387
+ * the applier sends them, with the grant declarations behind it. A grantee
388
+ * that only lacks declared grants has nothing pending: each declaration
389
+ * reports those as `absent` drift of its own (#4582). A grantee the server
390
+ * does not have is the thin diff's missing.
391
+ */
392
+ export async function pendingGrants(target, entities, held) {
393
+ const declared = grantDeclarations(entities);
394
+ if (declared.length === 0)
395
+ return [];
396
+ const live = held ?? (await heldGrants(target, entities));
327
397
  const atom = (json) => JSON.parse(json);
328
398
  const out = [];
329
399
  for (const g of declared) {
400
+ const atoms = live.get(g.grantee);
401
+ if (!atoms)
402
+ continue;
403
+ const have = Object.fromEntries(atoms.map((a) => [atomKey(a), JSON.stringify(a)]));
330
404
  const want = grantsObject(g.ddl, g.grantee, target.defaultDatabase).access ?? {};
331
- const have = held.get(g.grantee) ?? {};
332
- const statements = [
333
- ...Object.keys(have)
334
- .filter((k) => !(k in want))
335
- .map((k) => revokeStatement(atom(have[k]), g.grantee)),
336
- ...Object.keys(want)
337
- .filter((k) => !(k in have))
338
- .map((k) => grantStatement(atom(want[k]), g.grantee)),
339
- ];
340
- if (statements.length === 0)
405
+ const revokes = Object.keys(have)
406
+ .filter((k) => !(k in want))
407
+ .map((k) => revokeStatement(atom(have[k]), g.grantee));
408
+ if (revokes.length === 0)
341
409
  continue;
342
- out.push({ subject: `grants ${g.grantee}`, change: statements.join("; "), entities: [...g.exports].sort() });
410
+ const grants = Object.keys(want)
411
+ .filter((k) => !(k in have))
412
+ .map((k) => grantStatement(atom(want[k]), g.grantee));
413
+ out.push({ subject: `grants ${g.grantee}`, change: [...revokes, ...grants].join("; "), entities: [...g.exports].sort() });
343
414
  }
344
415
  return out;
345
416
  }