@google-cloud/kcmd 0.0.0-stage → 0.2.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.
Files changed (150) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +264 -2
  3. package/build/ts/kcmd/cli.js +20252 -0
  4. package/build/ts/kcmd/gcp/alloydb.d.ts +12 -0
  5. package/build/ts/kcmd/gcp/alloydb.js +70 -0
  6. package/build/ts/kcmd/gcp/api.d.ts +29 -0
  7. package/build/ts/kcmd/gcp/api.js +162 -0
  8. package/build/ts/kcmd/gcp/biglake.d.ts +18 -0
  9. package/build/ts/kcmd/gcp/biglake.js +59 -0
  10. package/build/ts/kcmd/gcp/bigquery.d.ts +57 -0
  11. package/build/ts/kcmd/gcp/bigquery.js +134 -0
  12. package/build/ts/kcmd/gcp/context.d.ts +10 -0
  13. package/build/ts/kcmd/gcp/context.js +74 -0
  14. package/build/ts/kcmd/gcp/crm.d.ts +13 -0
  15. package/build/ts/kcmd/gcp/crm.js +91 -0
  16. package/build/ts/kcmd/gcp/dataplex.d.ts +147 -0
  17. package/build/ts/kcmd/gcp/dataplex.js +406 -0
  18. package/build/ts/kcmd/gcp/index.d.ts +3 -0
  19. package/build/ts/kcmd/gcp/index.js +21 -0
  20. package/build/ts/kcmd/gcp/spanner.d.ts +73 -0
  21. package/build/ts/kcmd/gcp/spanner.js +126 -0
  22. package/build/ts/kcmd/index.d.ts +7 -0
  23. package/build/ts/kcmd/index.js +46 -0
  24. package/build/ts/kcmd/layout.d.ts +16 -0
  25. package/build/ts/kcmd/layout.js +31 -0
  26. package/build/ts/kcmd/layouts/documents.d.ts +24 -0
  27. package/build/ts/kcmd/layouts/documents.js +264 -0
  28. package/build/ts/kcmd/layouts/okf.d.ts +31 -0
  29. package/build/ts/kcmd/layouts/okf.js +386 -0
  30. package/build/ts/kcmd/layouts/semantic-model.d.ts +74 -0
  31. package/build/ts/kcmd/layouts/semantic-model.js +621 -0
  32. package/build/ts/kcmd/layouts/standard.d.ts +13 -0
  33. package/build/ts/kcmd/layouts/standard.js +108 -0
  34. package/build/ts/kcmd/manifest.d.ts +34 -0
  35. package/build/ts/kcmd/manifest.js +262 -0
  36. package/build/ts/kcmd/metadata.d.ts +25 -0
  37. package/build/ts/kcmd/metadata.js +8 -0
  38. package/build/ts/kcmd/okf/catalog_residue.d.ts +14 -0
  39. package/build/ts/kcmd/okf/catalog_residue.js +124 -0
  40. package/build/ts/kcmd/okf/concept.d.ts +14 -0
  41. package/build/ts/kcmd/okf/concept.js +110 -0
  42. package/build/ts/kcmd/okf/concept_to_entry.d.ts +10 -0
  43. package/build/ts/kcmd/okf/concept_to_entry.js +281 -0
  44. package/build/ts/kcmd/okf/init_okf_workspace.d.ts +15 -0
  45. package/build/ts/kcmd/okf/init_okf_workspace.js +113 -0
  46. package/build/ts/kcmd/okf/links.d.ts +23 -0
  47. package/build/ts/kcmd/okf/links.js +468 -0
  48. package/build/ts/kcmd/okf/okf_metadata.d.ts +12 -0
  49. package/build/ts/kcmd/okf/okf_metadata.js +99 -0
  50. package/build/ts/kcmd/semantic/bigquery.d.ts +22 -0
  51. package/build/ts/kcmd/semantic/bigquery.js +469 -0
  52. package/build/ts/kcmd/semantic/binding.d.ts +3 -0
  53. package/build/ts/kcmd/semantic/binding.js +92 -0
  54. package/build/ts/kcmd/semantic/catalog_entry.d.ts +24 -0
  55. package/build/ts/kcmd/semantic/catalog_entry.js +35 -0
  56. package/build/ts/kcmd/semantic/converters/owl/convert.d.ts +26 -0
  57. package/build/ts/kcmd/semantic/converters/owl/convert.js +41 -0
  58. package/build/ts/kcmd/semantic/converters/owl/model.d.ts +98 -0
  59. package/build/ts/kcmd/semantic/converters/owl/model.js +30 -0
  60. package/build/ts/kcmd/semantic/converters/owl/parse.d.ts +11 -0
  61. package/build/ts/kcmd/semantic/converters/owl/parse.js +634 -0
  62. package/build/ts/kcmd/semantic/converters/owl/to_ir.d.ts +19 -0
  63. package/build/ts/kcmd/semantic/converters/owl/to_ir.js +390 -0
  64. package/build/ts/kcmd/semantic/deploy_bigquery.d.ts +20 -0
  65. package/build/ts/kcmd/semantic/deploy_bigquery.js +226 -0
  66. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +104 -0
  67. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +1367 -0
  68. package/build/ts/kcmd/semantic/deploy_spanner.d.ts +15 -0
  69. package/build/ts/kcmd/semantic/deploy_spanner.js +150 -0
  70. package/build/ts/kcmd/semantic/deployment_target.d.ts +93 -0
  71. package/build/ts/kcmd/semantic/deployment_target.js +379 -0
  72. package/build/ts/kcmd/semantic/graph_ddl.d.ts +44 -0
  73. package/build/ts/kcmd/semantic/graph_ddl.js +656 -0
  74. package/build/ts/kcmd/semantic/import_model.d.ts +72 -0
  75. package/build/ts/kcmd/semantic/import_model.js +1698 -0
  76. package/build/ts/kcmd/semantic/ir.d.ts +589 -0
  77. package/build/ts/kcmd/semantic/ir.js +247 -0
  78. package/build/ts/kcmd/semantic/kc_actions.d.ts +45 -0
  79. package/build/ts/kcmd/semantic/kc_actions.js +531 -0
  80. package/build/ts/kcmd/semantic/kc_constraints.d.ts +44 -0
  81. package/build/ts/kcmd/semantic/kc_constraints.js +279 -0
  82. package/build/ts/kcmd/semantic/kc_converter.d.ts +37 -0
  83. package/build/ts/kcmd/semantic/kc_converter.js +839 -0
  84. package/build/ts/kcmd/semantic/kc_custom_types.d.ts +63 -0
  85. package/build/ts/kcmd/semantic/kc_custom_types.js +751 -0
  86. package/build/ts/kcmd/semantic/kc_entries.d.ts +155 -0
  87. package/build/ts/kcmd/semantic/kc_entries.js +284 -0
  88. package/build/ts/kcmd/semantic/kc_ids.d.ts +102 -0
  89. package/build/ts/kcmd/semantic/kc_ids.js +188 -0
  90. package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +29 -0
  91. package/build/ts/kcmd/semantic/knowledge_catalog.js +960 -0
  92. package/build/ts/kcmd/semantic/loader.d.ts +258 -0
  93. package/build/ts/kcmd/semantic/loader.js +3576 -0
  94. package/build/ts/kcmd/semantic/names.d.ts +10 -0
  95. package/build/ts/kcmd/semantic/names.js +85 -0
  96. package/build/ts/kcmd/semantic/osi_converter.d.ts +60 -0
  97. package/build/ts/kcmd/semantic/osi_converter.js +910 -0
  98. package/build/ts/kcmd/semantic/parameters.d.ts +19 -0
  99. package/build/ts/kcmd/semantic/parameters.js +171 -0
  100. package/build/ts/kcmd/semantic/pull_kc.d.ts +10 -0
  101. package/build/ts/kcmd/semantic/pull_kc.js +272 -0
  102. package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +59 -0
  103. package/build/ts/kcmd/semantic/resolve_inheritance.js +346 -0
  104. package/build/ts/kcmd/semantic/resolve_profiles.d.ts +291 -0
  105. package/build/ts/kcmd/semantic/resolve_profiles.js +2003 -0
  106. package/build/ts/kcmd/semantic/runtime/agent_tools.d.ts +93 -0
  107. package/build/ts/kcmd/semantic/runtime/agent_tools.js +243 -0
  108. package/build/ts/kcmd/semantic/runtime/dialect.d.ts +13 -0
  109. package/build/ts/kcmd/semantic/runtime/dialect.js +39 -0
  110. package/build/ts/kcmd/semantic/runtime/runtime.d.ts +88 -0
  111. package/build/ts/kcmd/semantic/runtime/runtime.js +301 -0
  112. package/build/ts/kcmd/semantic/runtime/store.d.ts +112 -0
  113. package/build/ts/kcmd/semantic/runtime/store.js +344 -0
  114. package/build/ts/kcmd/semantic/skills.d.ts +110 -0
  115. package/build/ts/kcmd/semantic/skills.js +697 -0
  116. package/build/ts/kcmd/semantic/source_dialect.d.ts +17 -0
  117. package/build/ts/kcmd/semantic/source_dialect.js +311 -0
  118. package/build/ts/kcmd/semantic/source_entries.d.ts +12 -0
  119. package/build/ts/kcmd/semantic/source_entries.js +341 -0
  120. package/build/ts/kcmd/semantic/spanner.d.ts +18 -0
  121. package/build/ts/kcmd/semantic/spanner.js +60 -0
  122. package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +23 -0
  123. package/build/ts/kcmd/semantic/sql_expr_utils.js +394 -0
  124. package/build/ts/kcmd/semantic/sql_identifiers.d.ts +6 -0
  125. package/build/ts/kcmd/semantic/sql_identifiers.js +267 -0
  126. package/build/ts/kcmd/semantic/sql_parser.d.ts +47 -0
  127. package/build/ts/kcmd/semantic/sql_parser.js +400 -0
  128. package/build/ts/kcmd/semantic/transpile.d.ts +76 -0
  129. package/build/ts/kcmd/semantic/transpile.js +581 -0
  130. package/build/ts/kcmd/semantic/validate.d.ts +70 -0
  131. package/build/ts/kcmd/semantic/validate.js +2266 -0
  132. package/build/ts/kcmd/semantic/yaml_options.d.ts +6 -0
  133. package/build/ts/kcmd/semantic/yaml_options.js +58 -0
  134. package/build/ts/kcmd/semantic/yaml_update.d.ts +26 -0
  135. package/build/ts/kcmd/semantic/yaml_update.js +782 -0
  136. package/build/ts/kcmd/snapshot.d.ts +37 -0
  137. package/build/ts/kcmd/snapshot.js +386 -0
  138. package/build/ts/kcmd/source.d.ts +31 -0
  139. package/build/ts/kcmd/source.js +119 -0
  140. package/build/ts/kcmd/sources/bq-dataset.d.ts +17 -0
  141. package/build/ts/kcmd/sources/bq-dataset.js +131 -0
  142. package/build/ts/kcmd/sources/entrygroup.d.ts +21 -0
  143. package/build/ts/kcmd/sources/entrygroup.js +83 -0
  144. package/build/ts/kcmd/sources/kb.d.ts +21 -0
  145. package/build/ts/kcmd/sources/kb.js +82 -0
  146. package/build/ts/kcmd/sources/semantic-model.d.ts +18 -0
  147. package/build/ts/kcmd/sources/semantic-model.js +48 -0
  148. package/build/ts/kcmd/sync.d.ts +35 -0
  149. package/build/ts/kcmd/sync.js +734 -0
  150. package/package.json +62 -4
@@ -0,0 +1,15 @@
1
+ import * as context from '../gcp/context';
2
+ import { LoadedModel } from './loader';
3
+ export interface DeployOptions {
4
+ validateOnly?: boolean;
5
+ maxOperationPolls?: number;
6
+ pollBackoffMs?: number;
7
+ }
8
+ export interface DeployResult {
9
+ success: boolean;
10
+ details?: string;
11
+ ddl: string[];
12
+ warnings: string[];
13
+ deployed: number;
14
+ }
15
+ export declare function deploySpanner(models: LoadedModel[], ctx: context.ApiContext, options?: DeployOptions): Promise<DeployResult>;
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ // Deploys a semantic model's Spanner Graph.
3
+ //
4
+ // This is the Spanner leg of `kcmd push` for the semantic-model scope, a
5
+ // sibling to deploy_bigquery: it consumes models already parsed into the
6
+ // semantic IR (see loadSemanticModels, shared across legs so a multi-destination
7
+ // push parses each document once), lowers each to `CREATE OR REPLACE PROPERTY
8
+ // GRAPH` DDL (generator: ./spanner), and applies that DDL to the Spanner
9
+ // database named by the model's GOOGLE deployment target.
10
+ //
11
+ // Spanner DDL is asynchronous: updateDatabaseDdl returns a long-running
12
+ // operation which this leg polls to completion, unlike the BigQuery leg's
13
+ // jobs.query.
14
+ //
15
+ // This is a library module: it emits no console output. The generated DDL and
16
+ // any loader/generator warnings are returned in `DeployResult` for the CLI
17
+ // (src/tool/commands.ts) to print.
18
+ //
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.deploySpanner = deploySpanner;
21
+ const spanner_1 = require("../gcp/spanner");
22
+ const deployment_target_1 = require("./deployment_target");
23
+ const spanner_2 = require("./spanner");
24
+ // Upper bound on getOperation polls before we give up on an operation that
25
+ // never reports completion.
26
+ const MAX_OPERATION_POLLS = 60;
27
+ // Pause between polls of a running operation.
28
+ const POLL_BACKOFF_MS = 1000;
29
+ function sleep(ms) {
30
+ return new Promise(resolve => setTimeout(resolve, ms));
31
+ }
32
+ // Waits for an updateDatabaseDdl long-running operation to complete and reports
33
+ // any terminal error. The initial POST returns an Operation (usually not yet
34
+ // `done`); we poll getOperation on its `name` until it is, then judge success
35
+ // by its `error` field.
36
+ async function awaitOperationDone(spanner, started, maxPolls, backoffMs) {
37
+ if (started.status !== 200) {
38
+ return { ok: false, error: started.message || String(started.status) };
39
+ }
40
+ let op = started.result;
41
+ if (!op) {
42
+ return {
43
+ ok: false,
44
+ error: started.message ||
45
+ 'updateDatabaseDdl returned status 200 with no operation body',
46
+ };
47
+ }
48
+ const opName = op.name;
49
+ let polls = 0;
50
+ while (!op?.done && opName && polls < maxPolls) {
51
+ if (polls > 0) {
52
+ await sleep(backoffMs);
53
+ }
54
+ polls++;
55
+ const res = await spanner.getOperation(opName);
56
+ if (res.status !== 200) {
57
+ return { ok: false, error: res.message || String(res.status) };
58
+ }
59
+ op = res.result;
60
+ }
61
+ if (!op?.done) {
62
+ return {
63
+ ok: false,
64
+ error: opName ?
65
+ `operation ${opName} did not complete after ${maxPolls} polls` :
66
+ 'updateDatabaseDdl returned no operation name to poll',
67
+ };
68
+ }
69
+ if (op.error) {
70
+ return { ok: false, error: op.error.message || `code ${op.error.code}` };
71
+ }
72
+ return { ok: true };
73
+ }
74
+ // Deploys the Spanner Graph for each pre-loaded model. Emits no console output;
75
+ // the generated DDL and any warnings are returned for the caller to print. The
76
+ // models are parsed once by the caller (loadSemanticModels) and shared with the
77
+ // other legs; each carries its originating document name for diagnostics.
78
+ async function deploySpanner(models, ctx, options = {}) {
79
+ const spanner = new spanner_1.SpannerClient(ctx);
80
+ const ddl = [];
81
+ const warnings = [];
82
+ let deployed = 0;
83
+ let modelsSeen = 0;
84
+ const fail = (details) => ({ success: false, details, ddl, warnings, deployed });
85
+ const maxPolls = options.maxOperationPolls ?? MAX_OPERATION_POLLS;
86
+ const backoffMs = options.pollBackoffMs ?? POLL_BACKOFF_MS;
87
+ for (const { document, model } of models) {
88
+ modelsSeen++;
89
+ let targets;
90
+ let malformed;
91
+ try {
92
+ ({ targets, malformed } = (0, deployment_target_1.spannerGraphTargets)(model));
93
+ }
94
+ catch (err) {
95
+ return fail(`Model '${model.name}' (${document}): ${err.message || err}`);
96
+ }
97
+ if (!targets.length) {
98
+ // A malformed-but-present target must not be reported as "none declared".
99
+ if (malformed.length) {
100
+ return fail(`Model '${model.name}' (${document}) declares Spanner Graph ` +
101
+ `deploymentTarget(s) that could not be parsed: ` +
102
+ `${malformed.join(', ')}. Expected ${deployment_target_1.SPANNER_GRAPH_FORM}.`);
103
+ }
104
+ return fail(`Model '${model.name}' (${document}) declares no Spanner Graph ` +
105
+ `deploymentTarget in a GOOGLE custom_extension; nothing to deploy.`);
106
+ }
107
+ for (const bad of malformed) {
108
+ warnings.push(`[${model.name}] ignoring unparseable Spanner Graph target: ${bad}`);
109
+ }
110
+ for (const target of targets) {
111
+ const gen = (0, spanner_2.generateSpannerPropertyGraph)(model, {
112
+ graphName: target.graphName,
113
+ });
114
+ for (const w of gen.warnings) {
115
+ warnings.push(`[${model.name} -> ${target.graphName}] ${w}`);
116
+ }
117
+ ddl.push(`-- ${target.uri}\n${gen.ddl}`);
118
+ if (options.validateOnly) {
119
+ continue;
120
+ }
121
+ // updateDatabaseDdl takes each statement WITHOUT a trailing semicolon:
122
+ // Spanner's DDL parser rejects one ("Expecting 'EOF' but found ';'" --
123
+ // verified live), unlike BigQuery's jobs.query, which accepts it. The
124
+ // generator terminates its DDL with `;` for the printed/golden artifact,
125
+ // so strip it here for the wire statement only.
126
+ const statement = gen.ddl.replace(/;\s*$/, '');
127
+ const started = await spanner.updateDatabaseDdl(target.project, target.instance, target.database, [statement]);
128
+ const outcome = await awaitOperationDone(spanner, started, maxPolls, backoffMs);
129
+ if (!outcome.ok) {
130
+ // CREATE OR REPLACE statements are applied one graph at a time, so any
131
+ // graphs already deployed in this run have mutated the database and are
132
+ // not rolled back. Surface that alongside the failing target.
133
+ const partial = deployed ?
134
+ ` (${deployed} graph(s) already deployed in this run; ` +
135
+ `CREATE OR REPLACE changes are not rolled back)` :
136
+ '';
137
+ return fail(`Failed to deploy '${target.graphName}': ${outcome.error}${partial}`);
138
+ }
139
+ deployed++;
140
+ }
141
+ }
142
+ if (!modelsSeen) {
143
+ if (options.validateOnly) {
144
+ warnings.push('No semantic model documents found; nothing to validate.');
145
+ return { success: true, ddl, warnings, deployed };
146
+ }
147
+ return fail('No semantic model documents found; nothing to deploy.');
148
+ }
149
+ return { success: true, ddl, warnings, deployed };
150
+ }
@@ -0,0 +1,93 @@
1
+ import { CustomExtension, DeploymentSpec, SemanticModel } from './ir';
2
+ export declare const BQ_GRAPH_FORM = "bigquery.googleapis.com/projects/<p>/datasets/<d>/propertyGraphs/<g>";
3
+ export declare const SPANNER_GRAPH_FORM = "spanner.googleapis.com/projects/<p>/instances/<i>/databases/<db>/graphs/<g>";
4
+ export declare const ALLOYDB_FORM = "alloydb.googleapis.com/projects/<p>/locations/<l>/clusters/<c>/databases/<db>";
5
+ export interface BigQueryGraphTarget {
6
+ project: string;
7
+ dataset: string;
8
+ graphName: string;
9
+ uri: string;
10
+ }
11
+ export interface SpannerGraphTarget {
12
+ project: string;
13
+ instance: string;
14
+ database: string;
15
+ graphName: string;
16
+ uri: string;
17
+ }
18
+ export interface AlloyDbTarget {
19
+ project: string;
20
+ location: string;
21
+ cluster: string;
22
+ database: string;
23
+ uri: string;
24
+ }
25
+ export interface GoogleDeploymentTargets {
26
+ uris: string[];
27
+ bigQuery: BigQueryGraphTarget[];
28
+ spanner: SpannerGraphTarget[];
29
+ alloyDb: AlloyDbTarget[];
30
+ malformed: string[];
31
+ }
32
+ export type ParsedDeploymentTarget = {
33
+ engine: 'bigquery';
34
+ coordinates: BigQueryGraphTarget;
35
+ } | {
36
+ engine: 'spanner';
37
+ coordinates: SpannerGraphTarget;
38
+ } | {
39
+ engine: 'alloydb';
40
+ coordinates: AlloyDbTarget;
41
+ };
42
+ export type ResolvedDeployment = ParsedDeploymentTarget & {
43
+ name: string;
44
+ profile: string;
45
+ };
46
+ /** Why a deployment's target is not accepted in a `deployments:` list. */
47
+ export declare function deploymentTargetError(model: SemanticModel, d: DeploymentSpec): string;
48
+ /** Why a target in a GOOGLE block's `deploymentTargets` is not accepted. */
49
+ export declare function googleBlockTargetError(model: SemanticModel, target: string): string;
50
+ /**
51
+ * Why a push would reject `target`, or undefined when it accepts it. `at` says
52
+ * where it is written.
53
+ */
54
+ export declare function targetProblem(at: string, target: string): string | undefined;
55
+ /**
56
+ * The deployment names a `--deployment` flag selects, or undefined when the
57
+ * flag is absent or negated. The flag is repeated or comma-separated. It
58
+ * arrives as a string, as an array of them, or as a boolean when
59
+ * it is absent or negated. Pieces are trimmed, empty ones dropped, and a
60
+ * repeated name kept once, at its first position. Throws when any occurrence
61
+ * of the flag names no deployment. A bare occurrence in an array
62
+ * arrives as `true` and names none.
63
+ */
64
+ export declare function parseDeploymentSelection(raw?: string | boolean | Array<string | boolean>): string[] | undefined;
65
+ /**
66
+ * A model's deployments with each target parsed into its engine and
67
+ * coordinates. Returns every deployment in declaration order, or only the
68
+ * `selected` names, in the order given. Fails on a selected name the model
69
+ * does not declare, and on a target in no accepted form.
70
+ */
71
+ export declare function resolveModelDeployments(model: SemanticModel, selected?: string[]): {
72
+ deployments: ResolvedDeployment[];
73
+ } | {
74
+ error: string;
75
+ };
76
+ export declare function googleBlockTargets(exts: readonly CustomExtension[] | undefined, modelName: string): string[];
77
+ export declare function unnamedDeployments(targets: readonly string[]): Array<{
78
+ name: string;
79
+ target: string;
80
+ }>;
81
+ export declare function googleDeploymentTargets(model: SemanticModel): GoogleDeploymentTargets;
82
+ export declare function bigQueryGraphTargets(model: SemanticModel): {
83
+ targets: BigQueryGraphTarget[];
84
+ malformed: string[];
85
+ };
86
+ export declare function spannerGraphTargets(model: SemanticModel): {
87
+ targets: SpannerGraphTarget[];
88
+ malformed: string[];
89
+ };
90
+ export declare function alloyDbTargets(model: SemanticModel): {
91
+ targets: AlloyDbTarget[];
92
+ malformed: string[];
93
+ };
@@ -0,0 +1,379 @@
1
+ "use strict";
2
+ // Reads where a model deploys and classifies each declared target.
3
+ //
4
+ // A semantic model names where it deploys with its `deployments:` list. Each
5
+ // entry has a name, a target and optionally the binding profile it uses. The
6
+ // loader also folds every target into `deploymentTargets` in a GOOGLE
7
+ // `custom_extensions` block, which is also where a vanilla document's GOOGLE
8
+ // block carries them. Three destination types are recognized here, each a
9
+ // GCP resource name, written with no leading `//`:
10
+ //
11
+ // BigQuery Graph:
12
+ // bigquery.googleapis.com/projects/<p>/datasets/<d>/propertyGraphs/<g>
13
+ // Spanner Graph:
14
+ // spanner.googleapis.com/projects/<p>/instances/<i>/databases/<db>/graphs/<g>
15
+ // AlloyDB database:
16
+ // alloydb.googleapis.com/projects/<p>/locations/<l>/clusters/<c>/databases/<db>
17
+ //
18
+ // The first two name a GRAPH; the third names a DATABASE, and the difference is
19
+ // not an oversight. AlloyDB has no property-graph DDL, so there is no graph to
20
+ // address and a URI pretending otherwise would name something that cannot be
21
+ // created. What an AlloyDB target says is where the model's rows live -- which
22
+ // is the whole of what a runtime needs in order to read an entity and carry out
23
+ // an action. A model bound to it deploys to Knowledge Catalog and runs; it does
24
+ // not deploy a graph, and push says so in a note rather than reporting the URI
25
+ // as a typo.
26
+ //
27
+ // The deploy legs (deploy_bigquery, deploy_spanner) and the push-time validator
28
+ // all derive their targets from this one reader, so a multi-destination push
29
+ // parses each model's targets once rather than once per reader.
30
+ //
31
+ // The capture groups are restricted to valid identifier characters: the
32
+ // components are interpolated into DDL unescaped (see the generators), so a
33
+ // permissive `[^/]+` would let backticks or semicolons through. A URI that
34
+ // matches neither destination type is collected as `malformed` (rejected by the
35
+ // validator and named in errors) rather than silently skipped.
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.ALLOYDB_FORM = exports.SPANNER_GRAPH_FORM = exports.BQ_GRAPH_FORM = void 0;
38
+ exports.deploymentTargetError = deploymentTargetError;
39
+ exports.googleBlockTargetError = googleBlockTargetError;
40
+ exports.targetProblem = targetProblem;
41
+ exports.parseDeploymentSelection = parseDeploymentSelection;
42
+ exports.resolveModelDeployments = resolveModelDeployments;
43
+ exports.googleBlockTargets = googleBlockTargets;
44
+ exports.unnamedDeployments = unnamedDeployments;
45
+ exports.googleDeploymentTargets = googleDeploymentTargets;
46
+ exports.bigQueryGraphTargets = bigQueryGraphTargets;
47
+ exports.spannerGraphTargets = spannerGraphTargets;
48
+ exports.alloyDbTargets = alloyDbTargets;
49
+ const loader_1 = require("./loader");
50
+ const resolve_profiles_1 = require("./resolve_profiles");
51
+ // Our own vendor tag in the Ossie `custom_extensions` list.
52
+ const GOOGLE_VENDOR = 'GOOGLE';
53
+ // A BigQuery Graph deployment target.
54
+ const BQ_GRAPH_TARGET = /^\/\/bigquery\.googleapis\.com\/projects\/([A-Za-z0-9_-]+)\/datasets\/([A-Za-z0-9_-]+)\/propertyGraphs\/([A-Za-z0-9_-]+)$/;
55
+ // A Spanner Graph deployment target, named as Dataplex names the graph's
56
+ // entry: `graphs/<g>`, where a BigQuery graph is `propertyGraphs/<g>`.
57
+ // Instance and database ids follow Spanner's resource-id grammar (lowercase
58
+ // letters, digits, hyphens); the graph name is a GoogleSQL identifier. As with
59
+ // BigQuery, the segments are interpolated into DDL (the graph name) and into
60
+ // the Spanner Admin API path (project/instance/database), so the character
61
+ // classes stay strict.
62
+ const SPANNER_GRAPH_TARGET = /^\/\/spanner\.googleapis\.com\/projects\/([A-Za-z0-9_-]+)\/instances\/([A-Za-z0-9_-]+)\/databases\/([A-Za-z0-9_-]+)\/graphs\/([A-Za-z0-9_-]+)$/;
63
+ // A Spanner graph written with BigQuery's `propertyGraphs` segment, which names
64
+ // no Dataplex entry. Push rejects it and names the `graphs` form. Its segments
65
+ // follow SPANNER_GRAPH_TARGET's, so the name it suggests is accepted.
66
+ const SPANNER_PROPERTY_GRAPH = /^(spanner\.googleapis\.com\/projects\/[A-Za-z0-9_-]+\/instances\/[A-Za-z0-9_-]+\/databases\/[A-Za-z0-9_-]+)\/propertyGraphs\/([A-Za-z0-9_-]+)$/;
67
+ // An AlloyDB database. A database belongs to its cluster, and AlloyDB locates
68
+ // a cluster by region, which is why there is a `locations` segment here and
69
+ // none in the Spanner target. A database name is a PostgreSQL identifier, and
70
+ // the strict class serves the same purpose as it does above: the name is
71
+ // interpolated into a connection string and into generated SQL, so a
72
+ // permissive class would let a quote character through.
73
+ const ALLOYDB_TARGET = /^\/\/alloydb\.googleapis\.com\/projects\/([A-Za-z0-9_-]+)\/locations\/([A-Za-z0-9_-]+)\/clusters\/([A-Za-z0-9_-]+)\/databases\/([A-Za-z0-9_-]+)$/;
74
+ // The instance-based AlloyDB URI kcmd used before. It names no real resource,
75
+ // and no target parses as it. It exists only so targetError can name the
76
+ // cluster-based form to write instead.
77
+ const LEGACY_ALLOYDB_TARGET = /^\/\/alloydb\.googleapis\.com\/projects\/([A-Za-z0-9_-]+)\/locations\/([A-Za-z0-9_-]+)\/clusters\/([A-Za-z0-9_-]+)\/instances\/([A-Za-z0-9_-]+)\/databases\/([A-Za-z0-9_-]+)$/;
78
+ // The GCP resource name form of each destination type, for messages.
79
+ exports.BQ_GRAPH_FORM = 'bigquery.googleapis.com/projects/<p>/datasets/<d>/propertyGraphs/<g>';
80
+ exports.SPANNER_GRAPH_FORM = 'spanner.googleapis.com/projects/<p>/instances/<i>/databases/<db>/graphs/<g>';
81
+ exports.ALLOYDB_FORM = 'alloydb.googleapis.com/projects/<p>/locations/<l>/clusters/<c>/databases/<db>';
82
+ // The Knowledge Catalog name prefixes of a graph or an AlloyDB database, each
83
+ // with the resource name form to write instead. kcmd accepts no catalog name as
84
+ // a target. `bigquery:propertygraph:` is a deprecated synonym for
85
+ // `bigquery:graph:`.
86
+ const CATALOG_PREFIXES = [
87
+ ['bigquery:graph:', exports.BQ_GRAPH_FORM],
88
+ ['bigquery:propertygraph:', exports.BQ_GRAPH_FORM],
89
+ ['spanner:graph:', exports.SPANNER_GRAPH_FORM],
90
+ ['alloydb:', exports.ALLOYDB_FORM],
91
+ ];
92
+ // Parses a deployment target written as a GCP resource name. Returns undefined
93
+ // for any other form, the resource URI with its leading `//` included. `uri`
94
+ // in the result is the target as written.
95
+ function parseTarget(uri) {
96
+ if (uri.startsWith('//'))
97
+ return undefined;
98
+ const resource = (0, loader_1.asResourceUri)(uri);
99
+ const bq = resource.match(BQ_GRAPH_TARGET);
100
+ if (bq) {
101
+ return {
102
+ engine: 'bigquery',
103
+ coordinates: { project: bq[1], dataset: bq[2], graphName: bq[3], uri },
104
+ };
105
+ }
106
+ const sp = resource.match(SPANNER_GRAPH_TARGET);
107
+ if (sp) {
108
+ return {
109
+ engine: 'spanner',
110
+ coordinates: {
111
+ project: sp[1],
112
+ instance: sp[2],
113
+ database: sp[3],
114
+ graphName: sp[4],
115
+ uri,
116
+ },
117
+ };
118
+ }
119
+ const ad = resource.match(ALLOYDB_TARGET);
120
+ if (ad) {
121
+ return {
122
+ engine: 'alloydb',
123
+ coordinates: {
124
+ project: ad[1],
125
+ location: ad[2],
126
+ cluster: ad[3],
127
+ database: ad[4],
128
+ uri,
129
+ },
130
+ };
131
+ }
132
+ return undefined;
133
+ }
134
+ // The cluster-based URI of the database a legacy instance-based AlloyDB URI
135
+ // names.
136
+ function clusterUri(legacy) {
137
+ return `//alloydb.googleapis.com/projects/${legacy[1]}/locations/${legacy[2]}/clusters/${legacy[3]}/databases/${legacy[5]}`;
138
+ }
139
+ /** Why a deployment's target is not accepted in a `deployments:` list. */
140
+ function deploymentTargetError(model, d) {
141
+ return targetError(`Model '${model.name}': deployment '${d.name}' targets '${d.target}'`, d.target);
142
+ }
143
+ /** Why a target in a GOOGLE block's `deploymentTargets` is not accepted. */
144
+ function googleBlockTargetError(model, target) {
145
+ return targetError(`Model '${model.name}': the GOOGLE custom_extension's ` +
146
+ `'deploymentTargets' lists '${target}'`, target);
147
+ }
148
+ // The Dataplex name of a Spanner graph `target` names with `propertyGraphs`,
149
+ // or undefined for any other target.
150
+ function spannerGraphName(target) {
151
+ const m = (0, loader_1.asResourceName)(target).match(SPANNER_PROPERTY_GRAPH);
152
+ return m ? `${m[1]}/graphs/${m[2]}` : undefined;
153
+ }
154
+ /**
155
+ * Why a push would reject `target`, or undefined when it accepts it. `at` says
156
+ * where it is written.
157
+ */
158
+ function targetProblem(at, target) {
159
+ return parseTarget(target) ? undefined : targetError(at, target);
160
+ }
161
+ /** Why `target` is not accepted. `at` says where it is written. */
162
+ function targetError(at, target) {
163
+ const legacy = (0, loader_1.asResourceUri)(target).match(LEGACY_ALLOYDB_TARGET);
164
+ if (legacy) {
165
+ return `${at}, the instance-based AlloyDB name, which names no real ` +
166
+ `resource. An AlloyDB database belongs to its cluster; write '${(0, loader_1.asResourceName)(clusterUri(legacy))}'.`;
167
+ }
168
+ const name = (0, loader_1.asResourceName)(target);
169
+ if (name !== target && parseTarget(name)) {
170
+ return `${at}, which is ${(0, loader_1.resourceUriFix)(target)}.`;
171
+ }
172
+ const graph = spannerGraphName(target);
173
+ if (graph) {
174
+ return `${at}, which names a Spanner graph by 'propertyGraphs'; write ` +
175
+ `its Dataplex name: '${graph}'.`;
176
+ }
177
+ const form = CATALOG_PREFIXES.find(([prefix]) => target.startsWith(prefix))?.[1];
178
+ if (form) {
179
+ return `${at}, which is a Knowledge Catalog name. Write the GCP resource ` +
180
+ `name instead: '${form}'.`;
181
+ }
182
+ return `${at}, which is not a BigQuery graph, a Spanner graph or an ` +
183
+ `AlloyDB database. Write a GCP resource name: '${exports.BQ_GRAPH_FORM}', '${exports.SPANNER_GRAPH_FORM}' or '${exports.ALLOYDB_FORM}'.`;
184
+ }
185
+ /**
186
+ * The deployment names a `--deployment` flag selects, or undefined when the
187
+ * flag is absent or negated. The flag is repeated or comma-separated. It
188
+ * arrives as a string, as an array of them, or as a boolean when
189
+ * it is absent or negated. Pieces are trimmed, empty ones dropped, and a
190
+ * repeated name kept once, at its first position. Throws when any occurrence
191
+ * of the flag names no deployment. A bare occurrence in an array
192
+ * arrives as `true` and names none.
193
+ */
194
+ function parseDeploymentSelection(raw) {
195
+ if (raw === undefined || typeof raw === 'boolean')
196
+ return undefined;
197
+ const noNames = `--deployment names no deployment; give one or more ` +
198
+ `deployment names, comma-separated.`;
199
+ const names = [];
200
+ const parts = Array.isArray(raw) ? raw : [raw];
201
+ for (const part of parts) {
202
+ const pieces = typeof part === 'boolean' ?
203
+ [] :
204
+ String(part).split(',').map(p => p.trim()).filter(p => p);
205
+ if (!pieces.length)
206
+ throw new Error(noNames);
207
+ for (const name of pieces) {
208
+ if (!names.includes(name))
209
+ names.push(name);
210
+ }
211
+ }
212
+ if (!names.length)
213
+ throw new Error(noNames);
214
+ return names;
215
+ }
216
+ /**
217
+ * A model's deployments with each target parsed into its engine and
218
+ * coordinates. Returns every deployment in declaration order, or only the
219
+ * `selected` names, in the order given. Fails on a selected name the model
220
+ * does not declare, and on a target in no accepted form.
221
+ */
222
+ function resolveModelDeployments(model, selected) {
223
+ const declared = model.deployments ?? [];
224
+ let chosen = declared;
225
+ if (selected !== undefined) {
226
+ const byName = new Map(declared.map(d => [d.name, d]));
227
+ chosen = [];
228
+ for (const name of selected) {
229
+ const d = byName.get(name);
230
+ if (!d) {
231
+ const available = declared.length ? declared.map(x => x.name).join(', ') : 'none';
232
+ return {
233
+ error: `Model '${model.name}' has no deployment named '${name}' (available: ${available}).`,
234
+ };
235
+ }
236
+ chosen.push(d);
237
+ }
238
+ }
239
+ const deployments = [];
240
+ for (const d of chosen) {
241
+ const parsed = parseTarget(d.target);
242
+ if (!parsed)
243
+ return { error: deploymentTargetError(model, d) };
244
+ deployments.push({
245
+ ...parsed,
246
+ name: d.name,
247
+ profile: d.profile ?? resolve_profiles_1.DEFAULT_PROFILE,
248
+ });
249
+ }
250
+ return { deployments };
251
+ }
252
+ // Memoizes the parse per model object so the several callers within one push
253
+ // (validation, source-project lookup, per-leg routing, each deploy leg) share a
254
+ // single parse rather than re-running JSON.parse + the regexes once per reader.
255
+ // Keyed by model identity: the IR object is stable through a push (the loader
256
+ // and inheritance passes each hand on a fixed set of objects) and
257
+ // neither its deployments nor its customExtensions are mutated after loading.
258
+ // A caller that narrows `deployments` builds a new model object. The returned
259
+ // value is read-only by convention -- no caller mutates its arrays.
260
+ const targetsCache = new WeakMap();
261
+ // The targets in a model's GOOGLE custom_extension(s). The extension `data` is
262
+ // an opaque, vendor-serialized JSON string (the loader keeps it verbatim); we
263
+ // own its `deploymentTargets` shape. Throws on malformed extension JSON.
264
+ function extensionTargets(model) {
265
+ return googleBlockTargets(model.customExtensions, model.name);
266
+ }
267
+ // The targets listed in the GOOGLE blocks among `exts`, in order. A string
268
+ // target is trimmed, and a blank one is dropped. A padded target is the same
269
+ // target, so trimming here keeps the duplicate check and the stored list in
270
+ // step. An entry that is not a string is kept as its JSON text, which parses
271
+ // as no supported destination. Throws when a GOOGLE block's data is not valid
272
+ // JSON.
273
+ function googleBlockTargets(exts, modelName) {
274
+ const uris = [];
275
+ for (const ext of exts ?? []) {
276
+ if (ext.vendorName !== GOOGLE_VENDOR) {
277
+ continue;
278
+ }
279
+ let data;
280
+ try {
281
+ data = JSON.parse(ext.data);
282
+ }
283
+ catch {
284
+ throw new Error(`Model '${modelName}': GOOGLE custom_extension 'data' is not valid JSON.`);
285
+ }
286
+ const list = data?.deploymentTargets;
287
+ if (!Array.isArray(list)) {
288
+ continue;
289
+ }
290
+ for (const uri of list) {
291
+ if (typeof uri !== 'string') {
292
+ uris.push(JSON.stringify(uri));
293
+ }
294
+ else if (uri.trim() !== '') {
295
+ uris.push(uri.trim());
296
+ }
297
+ }
298
+ }
299
+ return uris;
300
+ }
301
+ // Deployments for targets whose names are not known: a pull reads only the
302
+ // targets back from the catalog. One target is `default`; several are
303
+ // `deployment_1`, `deployment_2`, and so on. Targets are trimmed, and blank
304
+ // ones dropped. A repeated target is dropped too, so the list loads as a
305
+ // `deployments:` list.
306
+ function unnamedDeployments(targets) {
307
+ const seen = new Set();
308
+ const kept = targets.map(t => t.trim()).filter(t => {
309
+ if (t === '' || seen.has(t))
310
+ return false;
311
+ seen.add(t);
312
+ return true;
313
+ });
314
+ return kept.map((target, i) => ({
315
+ name: kept.length === 1 ? 'default' : `deployment_${i + 1}`,
316
+ target,
317
+ }));
318
+ }
319
+ // Reads a model's deployment targets in a single pass and returns the facts
320
+ // every caller derives from them. The targets come from `model.deployments`
321
+ // when the model has it, and otherwise from its GOOGLE custom_extension(s).
322
+ // Never both: in the Google flavor, the loader's GOOGLE block holds a copy of
323
+ // the list. Throws on malformed extension JSON.
324
+ function googleDeploymentTargets(model) {
325
+ const cached = targetsCache.get(model);
326
+ if (cached)
327
+ return cached;
328
+ const uris = model.deployments !== undefined ?
329
+ model.deployments.map(d => d.target) :
330
+ extensionTargets(model);
331
+ const bigQuery = [];
332
+ const spanner = [];
333
+ const alloyDb = [];
334
+ const malformed = [];
335
+ for (const uri of uris) {
336
+ const parsed = parseTarget(uri);
337
+ if (parsed?.engine === 'bigquery') {
338
+ bigQuery.push(parsed.coordinates);
339
+ }
340
+ else if (parsed?.engine === 'spanner') {
341
+ spanner.push(parsed.coordinates);
342
+ }
343
+ else if (parsed?.engine === 'alloydb') {
344
+ alloyDb.push(parsed.coordinates);
345
+ }
346
+ else {
347
+ // Matches no supported destination type: collect it as malformed
348
+ // (rejected by the validator) rather than silently ignoring it.
349
+ malformed.push(uri);
350
+ }
351
+ }
352
+ const result = { uris, bigQuery, spanner, alloyDb, malformed };
353
+ targetsCache.set(model, result);
354
+ return result;
355
+ }
356
+ // The BigQuery Graph deployment targets a model declares, plus any URIs that
357
+ // parse as no supported destination type. A view over googleDeploymentTargets,
358
+ // preserving the historical shape the BigQuery leg and Knowledge Catalog leg
359
+ // consume. Note `malformed` excludes a valid Spanner Graph or AlloyDB target:
360
+ // each is a recognized destination, just not a BigQuery one, so neither is
361
+ // reported as a BigQuery typo.
362
+ function bigQueryGraphTargets(model) {
363
+ const { bigQuery, malformed } = googleDeploymentTargets(model);
364
+ return { targets: bigQuery, malformed };
365
+ }
366
+ // The Spanner Graph deployment targets a model declares, plus any URIs that
367
+ // parse as no supported destination type. Symmetric to bigQueryGraphTargets.
368
+ function spannerGraphTargets(model) {
369
+ const { spanner, malformed } = googleDeploymentTargets(model);
370
+ return { targets: spanner, malformed };
371
+ }
372
+ // The AlloyDB database targets a model declares. Symmetric to the two above in
373
+ // shape, but there is no deploy leg behind it: an AlloyDB target is a store to
374
+ // run against, not a graph to create, so the only callers are store resolution
375
+ // and the messages that explain why a graph push has nothing to do.
376
+ function alloyDbTargets(model) {
377
+ const { alloyDb, malformed } = googleDeploymentTargets(model);
378
+ return { targets: alloyDb, malformed };
379
+ }