@google-cloud/kcmd 0.0.0-stage → 0.1.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 +266 -2
  3. package/build/ts/kcmd/cli.js +19578 -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 +16 -0
  9. package/build/ts/kcmd/gcp/biglake.js +50 -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 +63 -0
  21. package/build/ts/kcmd/gcp/spanner.js +123 -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 +344 -0
  30. package/build/ts/kcmd/layouts/semantic-model.d.ts +61 -0
  31. package/build/ts/kcmd/layouts/semantic-model.js +542 -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 +104 -0
  42. package/build/ts/kcmd/okf/concept_to_entry.d.ts +8 -0
  43. package/build/ts/kcmd/okf/concept_to_entry.js +254 -0
  44. package/build/ts/kcmd/okf/init_okf_workspace.d.ts +15 -0
  45. package/build/ts/kcmd/okf/init_okf_workspace.js +115 -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 +82 -0
  50. package/build/ts/kcmd/semantic/bigquery.d.ts +22 -0
  51. package/build/ts/kcmd/semantic/bigquery.js +525 -0
  52. package/build/ts/kcmd/semantic/binding.d.ts +3 -0
  53. package/build/ts/kcmd/semantic/binding.js +97 -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 +129 -0
  67. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +1396 -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 +34 -0
  73. package/build/ts/kcmd/semantic/graph_ddl.js +563 -0
  74. package/build/ts/kcmd/semantic/import_model.d.ts +50 -0
  75. package/build/ts/kcmd/semantic/import_model.js +1275 -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 +31 -0
  83. package/build/ts/kcmd/semantic/kc_converter.js +824 -0
  84. package/build/ts/kcmd/semantic/kc_custom_types.d.ts +70 -0
  85. package/build/ts/kcmd/semantic/kc_custom_types.js +828 -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 +103 -0
  89. package/build/ts/kcmd/semantic/kc_ids.js +191 -0
  90. package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +32 -0
  91. package/build/ts/kcmd/semantic/knowledge_catalog.js +1043 -0
  92. package/build/ts/kcmd/semantic/loader.d.ts +262 -0
  93. package/build/ts/kcmd/semantic/loader.js +3440 -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 +911 -0
  98. package/build/ts/kcmd/semantic/parameters.d.ts +18 -0
  99. package/build/ts/kcmd/semantic/parameters.js +152 -0
  100. package/build/ts/kcmd/semantic/pull_kc.d.ts +14 -0
  101. package/build/ts/kcmd/semantic/pull_kc.js +346 -0
  102. package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +54 -0
  103. package/build/ts/kcmd/semantic/resolve_inheritance.js +330 -0
  104. package/build/ts/kcmd/semantic/resolve_profiles.d.ts +250 -0
  105. package/build/ts/kcmd/semantic/resolve_profiles.js +1822 -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 +94 -0
  111. package/build/ts/kcmd/semantic/runtime/runtime.js +318 -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 +10 -0
  117. package/build/ts/kcmd/semantic/source_dialect.js +261 -0
  118. package/build/ts/kcmd/semantic/source_entries.d.ts +12 -0
  119. package/build/ts/kcmd/semantic/source_entries.js +282 -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 +17 -0
  123. package/build/ts/kcmd/semantic/sql_expr_utils.js +221 -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 +44 -0
  127. package/build/ts/kcmd/semantic/sql_parser.js +378 -0
  128. package/build/ts/kcmd/semantic/transpile.d.ts +74 -0
  129. package/build/ts/kcmd/semantic/transpile.js +563 -0
  130. package/build/ts/kcmd/semantic/validate.d.ts +35 -0
  131. package/build/ts/kcmd/semantic/validate.js +1899 -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 +33 -0
  137. package/build/ts/kcmd/snapshot.js +379 -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 +697 -0
  150. package/package.json +62 -4
@@ -0,0 +1,18 @@
1
+ import { ActionParameter } from './ir';
2
+ export declare function isParameterRequired(param: ActionParameter): boolean;
3
+ export declare function storeCodeFor(dataType: string): string;
4
+ export declare function sentence(text: string): string;
5
+ /**
6
+ * One value, parsed to the store type its declared ontology type implies.
7
+ *
8
+ * Exported because a read has the same problem a write does: a filter on a
9
+ * typed column has to be bound AS that type, or the predicate needs a cast and
10
+ * no index can answer it. Sharing this also means one answer to what counts as
11
+ * an Integer or a Date, rather than one for writes and another for reads.
12
+ */
13
+ export declare function bindScalar(param: ActionParameter, raw: unknown): {
14
+ value: unknown;
15
+ code: string;
16
+ } | {
17
+ error: string;
18
+ };
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ // Leaf helpers for action parameter optionality, scalar type checking, and
3
+ // store type codes -- shared by deploy-time validation and run-time execution
4
+ // without pulling store clients into the validation layer.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isParameterRequired = isParameterRequired;
7
+ exports.storeCodeFor = storeCodeFor;
8
+ exports.sentence = sentence;
9
+ exports.bindScalar = bindScalar;
10
+ function isParameterRequired(param) {
11
+ if (param.required !== undefined)
12
+ return param.required;
13
+ return param.default === undefined;
14
+ }
15
+ function storeCodeFor(dataType) {
16
+ switch (dataType) {
17
+ case 'Integer':
18
+ return 'INT64';
19
+ case 'Float':
20
+ return 'FLOAT64';
21
+ case 'Decimal':
22
+ return 'NUMERIC';
23
+ case 'Boolean':
24
+ return 'BOOL';
25
+ case 'Date':
26
+ return 'DATE';
27
+ case 'DateTime':
28
+ case 'DateTimeTz':
29
+ return 'TIMESTAMP';
30
+ default:
31
+ return 'STRING';
32
+ }
33
+ }
34
+ // Ends a fragment that is about to be followed by another sentence.
35
+ function sentence(text) {
36
+ const trimmed = text.trim();
37
+ return /[.!?]$/.test(trimmed) ? trimmed : `${trimmed}.`;
38
+ }
39
+ // What Spanner accepts for a DATE and a TIMESTAMP parameter. The zone is
40
+ // required rather than defaulted, because a timestamp written without one
41
+ // means a different instant to every reader who supplies the missing part.
42
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
43
+ const RFC3339_TIMESTAMP = /^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[-+]\d{2}:?\d{2})$/;
44
+ // Whether `text` is a day that exists, written the one way Spanner reads.
45
+ // `Date.parse` answers neither question: it accepts '03/04/2026', and it reads
46
+ // '2026-02-30' as the second of March rather than rejecting it. A round trip
47
+ // answers both, because a day that rolled over comes back written differently.
48
+ function isCalendarDay(text) {
49
+ if (!ISO_DATE.test(text))
50
+ return false;
51
+ const utc = new Date(`${text}T00:00:00Z`);
52
+ return !Number.isNaN(utc.getTime()) && utc.toISOString().startsWith(text);
53
+ }
54
+ /**
55
+ * One value, parsed to the store type its declared ontology type implies.
56
+ *
57
+ * Exported because a read has the same problem a write does: a filter on a
58
+ * typed column has to be bound AS that type, or the predicate needs a cast and
59
+ * no index can answer it. Sharing this also means one answer to what counts as
60
+ * an Integer or a Date, rather than one for writes and another for reads.
61
+ */
62
+ function bindScalar(param, raw) {
63
+ // A parameter whose type resolved to nothing. The loader warns and validate
64
+ // refuses to push such a model, so this is reached only through the library
65
+ // entry point -- and there is no type to parse the value against, so there
66
+ // is nothing to bind.
67
+ const type = param.type;
68
+ if (type === undefined) {
69
+ return {
70
+ error: `Action parameter '${param.name}' has no type: it states no ` +
71
+ `scalar 'type' and projects no field that supplies one, so a value ` +
72
+ `cannot be bound to it.`,
73
+ };
74
+ }
75
+ // Everything below stringifies the value before matching it against the
76
+ // type, which is what lets a JSON `"12347"` bind as an Integer. An object or
77
+ // an array has a string form too -- `[object Object]` -- and it would sail
78
+ // through as a String and be written to the store verbatim. A parameter
79
+ // carries ONE scalar, so a composite is refused here rather than flattened.
80
+ if (raw !== undefined && raw !== null && typeof raw === 'object') {
81
+ return {
82
+ error: `Action parameter '${param.name}' (${type}) was given ${Array.isArray(raw) ? 'a list' :
83
+ 'an object'}, but a parameter ` +
84
+ `carries a single scalar value.`,
85
+ };
86
+ }
87
+ // An empty String IS a value: `--arg memo=` is the caller saying the memo is
88
+ // blank, which is a different statement from not passing one. For every
89
+ // other type there is no value empty text could be, so it stays an error.
90
+ if (raw === undefined || raw === null ||
91
+ (`${raw}`.trim() === '' && type !== 'String')) {
92
+ return {
93
+ error: `Action parameter '${param.name}' (${type}) was not given ` +
94
+ `a value.`,
95
+ };
96
+ }
97
+ const text = `${raw}`.trim();
98
+ const code = storeCodeFor(type);
99
+ switch (type) {
100
+ case 'Integer':
101
+ if (!/^[-+]?\d+$/.test(text)) {
102
+ return { error: `'${param.name}' is an Integer, but '${text}' is not.` };
103
+ }
104
+ // INT64 travels as a string over the REST surface; a JSON number would
105
+ // lose precision above 2^53.
106
+ return { value: text, code };
107
+ case 'Float':
108
+ if (!Number.isFinite(Number(text))) {
109
+ return { error: `'${param.name}' is a Float, but '${text}' is not.` };
110
+ }
111
+ return { value: Number(text), code };
112
+ case 'Decimal':
113
+ if (!/^[-+]?\d+(\.\d+)?$/.test(text)) {
114
+ return { error: `'${param.name}' is a Decimal, but '${text}' is not.` };
115
+ }
116
+ // NUMERIC travels as a string, for the same reason: an exact decimal
117
+ // routed through a JSON number stops being exact.
118
+ return { value: text, code };
119
+ case 'Boolean':
120
+ if (!/^(true|false)$/i.test(text)) {
121
+ return { error: `'${param.name}' is a Boolean, but '${text}' is not.` };
122
+ }
123
+ return { value: /^true$/i.test(text), code };
124
+ case 'Date':
125
+ // Spanner reads a DATE as YYYY-MM-DD and nothing else. '03/04/2026' is
126
+ // the fourth of March to one reader and the third of April to another,
127
+ // so the shape is checked -- and then the calendar, because a shape is
128
+ // not a day.
129
+ if (!isCalendarDay(text)) {
130
+ return {
131
+ error: `'${param.name}' is a Date, but '${text}' is not one. Dates are written YYYY-MM-DD.`,
132
+ };
133
+ }
134
+ return { value: text, code };
135
+ case 'DateTime':
136
+ case 'DateTimeTz':
137
+ if (!RFC3339_TIMESTAMP.test(text) || !isCalendarDay(text.slice(0, 10))) {
138
+ return {
139
+ error: `'${param.name}' is a ${type}, but '${text}' is not a timestamp. Timestamps are written like ` +
140
+ `2026-03-04T10:00:00Z, with the zone.`,
141
+ };
142
+ }
143
+ return { value: text, code };
144
+ default:
145
+ // `raw`, not `text`. The trim above exists to parse a number or a date
146
+ // off a command line; a String parameter is not parsed, it IS the value.
147
+ // Trimming here would store `see ticket` for `--arg memo=" see ticket "`
148
+ // -- the caller's text altered on the way to the store, by a rule
149
+ // nothing states.
150
+ return { value: `${raw}`, code };
151
+ }
152
+ }
@@ -0,0 +1,14 @@
1
+ import { CatalogClient } from '../gcp/dataplex';
2
+ import { ReadResult } from './kc_converter';
3
+ import { FileCopy } from './kc_custom_types';
4
+ export interface KcPullOptions {
5
+ project: string;
6
+ location: string;
7
+ entryGroup: string;
8
+ v2Aspects?: boolean;
9
+ systemTypeProject?: string;
10
+ }
11
+ export interface KcPullResult extends ReadResult {
12
+ rawFiles?: FileCopy[];
13
+ }
14
+ export declare function pullKnowledgeCatalog(cat: CatalogClient, opts: KcPullOptions): Promise<KcPullResult>;
@@ -0,0 +1,346 @@
1
+ "use strict";
2
+ // Pull: fetch a semantic model from a live Knowledge Catalog into the IR.
3
+ //
4
+ // The read-direction counterpart of `deployKnowledgeCatalog` (push, in
5
+ // `deploy_knowledge_catalog.ts`). Unlike a write, a pull needs no server-side
6
+ // type provisioning -- only that the `semantic-*` entries exist. It enumerates
7
+ // the entry group, keeps the semantic entries, hydrates each one's aspect data
8
+ // (a BASIC list omits aspect data, so each entry is re-fetched with its aspect
9
+ // types -- an entity needs BOTH its `semantic-entity` aspect and the built-in
10
+ // `schema` aspect), and hands the hydrated entries to the pure reader
11
+ // (`kc_converter.modelsFromCatalogResources`). With the first-generation
12
+ // aspects it also returns the file copies a push stored of the model and
13
+ // profile files, for the caller to write instead of the rebuilt model.
14
+ //
15
+ // TODO(#278): Layer 2 push/pull symmetry. This is the pull half over the
16
+ // pure `kc_converter` codec; the push half (`deployKnowledgeCatalog`) still
17
+ // lives in `deploy_knowledge_catalog.ts`. Once #278 merges, rename that
18
+ // file to `push_kc.ts` (repointing `src/tool/commands.ts` and its test) so
19
+ // the orchestration layer reads as `push_kc` / `pull_kc`. Rename only -- no
20
+ // logic moves.
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.pullKnowledgeCatalog = pullKnowledgeCatalog;
23
+ const dataplex_1 = require("../gcp/dataplex");
24
+ const kc_actions_1 = require("./kc_actions");
25
+ const kc_constraints_1 = require("./kc_constraints");
26
+ const kc_converter_1 = require("./kc_converter");
27
+ const kc_custom_types_1 = require("./kc_custom_types");
28
+ const kc_ids_1 = require("./kc_ids");
29
+ const sql_parser_1 = require("./sql_parser");
30
+ // Upper bound on in-flight aspect-hydration fetches during a pull.
31
+ const HYDRATE_CONCURRENCY = 8;
32
+ // Reads the semantic model back from a Knowledge Catalog entry group. Emits no
33
+ // console output; soft warnings (from the reader) are returned for the caller
34
+ // to print. Hard failures -- more than one model in the group, a fetch error
35
+ // on any entry or its links, or file copies that are not one finished push's
36
+ // -- throw, so the pull aborts rather than write a partial model.
37
+ async function pullKnowledgeCatalog(cat, opts) {
38
+ await (0, sql_parser_1.loadSqlEngine)();
39
+ const destination = `${opts.project}.${opts.location}.${opts.entryGroup}`;
40
+ const warnings = [];
41
+ // Enumerate the group (paging is inherently sequential) and pick the semantic
42
+ // entries, then hydrate their aspects concurrently: a BASIC list omits aspect
43
+ // data, so each entry needs its own lookupEntry, and those fetches are
44
+ // independent. The pool preserves input order so warnings stay deterministic.
45
+ const targets = [];
46
+ for await (const entry of cat.listEntries(opts.project, opts.location, opts.entryGroup)) {
47
+ // With v2Aspects on, a pull returns no file copies, so it fetches none.
48
+ if (opts.v2Aspects && isFileCopyEntry(entry))
49
+ continue;
50
+ const aspectTypes = semanticAspectTypes(entry.entryType, opts.v2Aspects);
51
+ if (aspectTypes)
52
+ targets.push({ entry, aspectTypes });
53
+ // else: not part of a semantic model; ignore it.
54
+ }
55
+ // An entry group holds exactly one semantic model. Zero anchors is a clean
56
+ // "nothing to pull" (the reader returns no models); more than one is an
57
+ // unexpected catalog state we refuse to guess through -- name the anchors and
58
+ // fail so it can be fixed at the source.
59
+ const anchors = targets.filter(t => t.entry.entryType?.endsWith('/entryTypes/semantic-model'));
60
+ if (anchors.length > 1) {
61
+ const ids = anchors.map(a => (0, kc_ids_1.entryIdOf)(a.entry.name)).sort();
62
+ throw new Error(`entry group ${destination} holds ${anchors.length} semantic models (${ids.join(', ')}); expected exactly one. Remove the extra anchor(s) ` +
63
+ `from the catalog and pull again.`);
64
+ }
65
+ // Hydrate every semantic entry. A failed fetch means part of the model would
66
+ // be silently missing, so abort rather than reconstruct an incomplete model.
67
+ const hydrated = await mapConcurrent(targets, HYDRATE_CONCURRENCY, async ({ entry, aspectTypes }) => opts.v2Aspects ?
68
+ fetchEveryAspect(cat, opts, entry, aspectTypes) :
69
+ fetchEntry(cat, opts, entry, aspectTypes));
70
+ // Second fetch pass: with the first-generation types, the schema-join links
71
+ // of the relationships, which listing entries does not return. Links are
72
+ // listed per entity entry and deduplicated, since each comes back from both
73
+ // endpoints, and a failed lookup aborts the pull. With v2Aspects on, pull
74
+ // reads no link.
75
+ const entityEntries = opts.v2Aspects ? [] : hydrated.filter(e => e.entryType?.endsWith('/entryTypes/semantic-entity'));
76
+ const linkLists = await mapConcurrent(entityEntries, HYDRATE_CONCURRENCY, async (entry) => {
77
+ const res = await cat.lookupEntryLinks(opts.project, opts.location, {
78
+ entry: entry.name,
79
+ entryLinkTypes: [new kc_ids_1.Namer(opts).typeName('entryLink', 'schema-join')],
80
+ });
81
+ if (res.status !== 200 || !res.result) {
82
+ throw new Error(`failed to fetch entry links for '${entry.name}' ` +
83
+ `(status ${res.status}); pull aborted`);
84
+ }
85
+ return res.result;
86
+ });
87
+ const seenLinks = new Set();
88
+ const entryLinks = [];
89
+ for (const links of linkLists) {
90
+ for (const link of links) {
91
+ const key = (0, kc_converter_1.linkDedupKey)(link);
92
+ if (seenLinks.has(key))
93
+ continue;
94
+ seenLinks.add(key);
95
+ entryLinks.push(link);
96
+ }
97
+ }
98
+ const read = (0, kc_converter_1.modelsFromCatalogResources)(hydrated, entryLinks, { v2Aspects: opts.v2Aspects });
99
+ if (opts.v2Aspects) {
100
+ warnings.push(...read.warnings);
101
+ return { ...read, warnings: [...new Set(warnings)] };
102
+ }
103
+ // A copy belongs to the group's one model, so with no model there are none.
104
+ if (!read.models.length) {
105
+ warnings.push(...read.warnings);
106
+ return { models: [], rawFiles: [], warnings: [...new Set(warnings)] };
107
+ }
108
+ // The first-generation aspects cannot hold the whole model, so the caller
109
+ // writes these copies instead of the model rebuilt from the aspects. That
110
+ // needs the copy of the model file, `<model>.yaml`, which is where pull
111
+ // writes the model. Without it the rebuilt model is written, as for a model
112
+ // pushed before copies were stored.
113
+ const model = read.models[0].name;
114
+ // The model's own copies sit under its anchor's `.files.` prefix. A copy
115
+ // another model left behind is not one of them.
116
+ const own = `${(0, kc_ids_1.entryIdOf)(anchors[0].entry.name)}.files.`;
117
+ const before = warnings.length;
118
+ const copies = readFileCopies(hydrated.filter(e => (0, kc_ids_1.entryIdOf)(e.name).startsWith(own)), warnings);
119
+ const skipped = warnings.slice(before);
120
+ // Every copy one push stores carries that push's id and how many copies it
121
+ // stored. Copies whose ids differ, or of which only some carry one, or a
122
+ // number of copies other than that count, mean the last push stopped
123
+ // partway. Writing them would mix files from two pushes or leave one out.
124
+ // A push that stores copies also labels the model entry with its push id,
125
+ // and writes that entry first. A labeled model whose copies are missing or
126
+ // carry another id means the last push stopped before storing them.
127
+ // Writing the copies of an earlier push, or rebuilding, would overwrite
128
+ // the local files. The hydrated entries keep the order of `targets`.
129
+ const count = copies[0]?.pushFileCount;
130
+ const anchor = hydrated[targets.indexOf(anchors[0])];
131
+ const label = anchor.entrySource?.labels?.[kc_custom_types_1.PUSH_ID_LABEL];
132
+ const unfinished = new Set(copies.map(f => f.pushId)).size > 1 ?
133
+ 'its file copies in the catalog come from different pushes' :
134
+ count !== undefined && copies.length < count ?
135
+ `the catalog holds ${copies.length} of its ${count} file copies` :
136
+ count !== undefined && copies.length > count ?
137
+ `the catalog holds ${copies.length} file copies, but the push stored ` +
138
+ `${count}` :
139
+ label !== undefined && !copies.length ?
140
+ (skipped.length ? 'the catalog holds no readable file copy' :
141
+ 'the catalog holds none of its file copies') :
142
+ label !== undefined && copies[0].pushId !== label ?
143
+ 'its model entry and its file copies come from different pushes' :
144
+ undefined;
145
+ if (unfinished) {
146
+ // A skipped copy is often why the count is off, so its warning goes with
147
+ // the error.
148
+ throw new Error(`the last push of model '${model}' did not finish: ${unfinished}, ` +
149
+ `so pull writes nothing. Push again, then pull.` +
150
+ skipped.map(w => ` ${w}.`).join(''));
151
+ }
152
+ if (copies.some(f => f.fileName === `${model}.yaml`)) {
153
+ // The rebuilt model is not written, so its warnings are left out.
154
+ return {
155
+ models: read.models,
156
+ rawFiles: copies,
157
+ warnings: [...new Set(warnings)],
158
+ };
159
+ }
160
+ warnings.push(...read.warnings);
161
+ warnings.push(copies.length ?
162
+ `the stored copies of model '${model}' hold no '${model}.yaml', so ` +
163
+ `the model file is rebuilt from the catalog's aspects and may ` +
164
+ `differ from what was pushed` :
165
+ 'no stored copy of the model file was found, so it is rebuilt from ' +
166
+ 'the catalog\'s aspects and may differ from what was pushed; ' +
167
+ 'push it again to store a copy');
168
+ return { models: read.models, rawFiles: [], warnings: [...new Set(warnings)] };
169
+ }
170
+ // Fetches `entry` with the aspects of `aspectTypes`, at `paths` when given.
171
+ // Throws on a failed fetch.
172
+ async function fetchEntry(cat, opts, entry, aspectTypes, paths) {
173
+ const res = await cat.lookupEntry(opts.project, opts.location, entry.name, { aspects: aspectTypes, paths });
174
+ if (res.status !== 200 || !res.result) {
175
+ throw new Error(`failed to fetch entry '${entry.name}' (status ${res.status}); pull aborted`);
176
+ }
177
+ return res.result;
178
+ }
179
+ // Most aspects Dataplex returns for one entry in one read.
180
+ const MAX_ASPECTS_PER_READ = 100;
181
+ // Most bytes of path and query Dataplex accepts in a GET.
182
+ const MAX_URL_BYTES = 16_384;
183
+ // Fetches `entry` with every aspect of `aspectTypes`, one read at a time. A
184
+ // read that returns MAX_ASPECTS_PER_READ is repeated once per type, and a type
185
+ // that returns that many is read again at the entry and at each field.
186
+ async function fetchEveryAspect(cat, opts, entry, aspectTypes) {
187
+ const full = (e) => Object.keys(e.aspects ?? {}).length >= MAX_ASPECTS_PER_READ;
188
+ const read = await fetchEntry(cat, opts, entry, aspectTypes);
189
+ if (!full(read))
190
+ return read;
191
+ const byType = [];
192
+ for (const t of aspectTypes) {
193
+ byType.push(await fetchEntry(cat, opts, entry, [t]));
194
+ }
195
+ const schema = byType[aspectTypes.findIndex(t => t.endsWith('/aspectTypes/schema'))];
196
+ const paths = [
197
+ '',
198
+ ...Object.values(schema?.aspects ?? {})
199
+ .flatMap(a => a.data?.fields ?? [])
200
+ .map(f => `Schema.${f.name}`)
201
+ ];
202
+ const byPath = [];
203
+ for (const t of aspectTypes.filter((_, i) => full(byType[i]))) {
204
+ for (const batch of pathBatches(opts, entry, t, paths)) {
205
+ byPath.push(await fetchEntry(cat, opts, entry, [t], batch));
206
+ }
207
+ }
208
+ const reads = [...byType, ...byPath];
209
+ return { ...read, aspects: Object.assign({}, ...reads.map(r => r.aspects)) };
210
+ }
211
+ // Splits `paths` into batches for reads of `type` on `entry`, each within
212
+ // MAX_URL_BYTES. A batch holds fewer than MAX_ASPECTS_PER_READ paths, and a
213
+ // path holds at most one instance of a type, so no read comes back cut.
214
+ function pathBatches(opts, entry, type, paths) {
215
+ // Bytes of `params` in the URL, with the '?' or '&' before them.
216
+ const bytes = (params) => new URLSearchParams(params).toString().length + 1;
217
+ const base = `/v1/${(0, dataplex_1.catalogContainer)(opts.project, opts.location)}:lookupEntry`
218
+ .length +
219
+ bytes([['entry', entry.name], ['view', 'CUSTOM'], ['aspectTypes', type]]);
220
+ const batches = [];
221
+ let batch = [];
222
+ let size = base;
223
+ for (const path of paths) {
224
+ const add = bytes([['paths', path]]);
225
+ if (batch.length === MAX_ASPECTS_PER_READ - 1 ||
226
+ size + add > MAX_URL_BYTES) {
227
+ batches.push(batch);
228
+ batch = [];
229
+ size = base;
230
+ }
231
+ batch.push(path);
232
+ size += add;
233
+ }
234
+ return [...batches, batch];
235
+ }
236
+ // True when an entry is a file copy, matched by its entry type's id suffix so
237
+ // the project the custom type lives in need not be known.
238
+ function isFileCopyEntry(entry) {
239
+ return entry.entryType?.endsWith(`/entryTypes/${kc_custom_types_1.FILE_COPY_TYPE_ID}`) ?? false;
240
+ }
241
+ // The file copies among the hydrated entries, sorted by file name, each with
242
+ // the push id and copy count it carries. A copy whose aspect holds no file
243
+ // name or no content is skipped with a warning.
244
+ function readFileCopies(entries, warnings) {
245
+ const files = [];
246
+ for (const entry of entries) {
247
+ if (!isFileCopyEntry(entry))
248
+ continue;
249
+ const aspect = Object.entries(entry.aspects ?? {}).find(([key, a]) => key.endsWith(`.${kc_custom_types_1.FILE_COPY_TYPE_ID}`) ||
250
+ (a.aspectType ?? '').endsWith(`/aspectTypes/${kc_custom_types_1.FILE_COPY_TYPE_ID}`));
251
+ const data = aspect?.[1].data;
252
+ if (typeof data?.fileName !== 'string' || data.fileName === '' ||
253
+ typeof data.content !== 'string') {
254
+ warnings.push(`file copy '${(0, kc_ids_1.entryIdOf)(entry.name)}' holds no file name or no ` +
255
+ `content; it is skipped`);
256
+ continue;
257
+ }
258
+ files.push({
259
+ fileName: data.fileName,
260
+ content: data.content,
261
+ ...(typeof data.pushId === 'string' ? { pushId: data.pushId } : {}),
262
+ ...(typeof data.pushFileCount === 'number' ?
263
+ { pushFileCount: data.pushFileCount } :
264
+ {}),
265
+ });
266
+ }
267
+ return files.sort((a, b) => a.fileName < b.fileName ? -1 : a.fileName > b.fileName ? 1 : 0);
268
+ }
269
+ // The aspect types each second-generation entry type is read with.
270
+ const OBJECT_ASPECTS = ['guidelines', 'semantic-extensions', 'overview'];
271
+ const V2_ASPECT_TYPES = new Map([
272
+ ['semantic-model', ['semantic-model', ...OBJECT_ASPECTS]],
273
+ [
274
+ 'semantic-entity',
275
+ ['semantic-entity', 'schema', 'sql-expressions', ...OBJECT_ASPECTS]
276
+ ],
277
+ [
278
+ 'semantic-metric', ['semantic-metric', 'sql-expressions', ...OBJECT_ASPECTS]
279
+ ],
280
+ ['semantic-relationship', ['semantic-relationship', ...OBJECT_ASPECTS]],
281
+ ]);
282
+ // The aspect types to read for a semantic entry, in its entry type's project
283
+ // and location: V2_ASPECT_TYPES with `v2Aspects` on, the first-generation set
284
+ // otherwise. Undefined for an entry outside a semantic model.
285
+ function semanticAspectTypes(entryType, v2Aspects) {
286
+ const marker = '/entryTypes/';
287
+ const idx = entryType?.indexOf(marker) ?? -1;
288
+ if (idx < 0)
289
+ return undefined;
290
+ const typeBase = entryType.slice(0, idx);
291
+ const t = entryType.slice(idx + marker.length);
292
+ const aspectType = (name) => `${typeBase}/aspectTypes/${name}`;
293
+ const v2Types = v2Aspects ? V2_ASPECT_TYPES.get(t) : undefined;
294
+ if (v2Types)
295
+ return v2Types.map(aspectType);
296
+ switch (t) {
297
+ case 'semantic-model':
298
+ return [aspectType('semantic-model'), aspectType('guidelines')];
299
+ case 'semantic-entity':
300
+ return [
301
+ aspectType('semantic-entity'), aspectType('schema'),
302
+ aspectType('guidelines')
303
+ ];
304
+ case 'semantic-metric':
305
+ return [aspectType('semantic-metric'), aspectType('guidelines')];
306
+ case kc_custom_types_1.ACTION_TYPE_ID:
307
+ // An action's entry type is custom (see kc_custom_types.ts), so it
308
+ // lives in the destination project rather than under `dataplex-types`;
309
+ // `typeBase` already points there, and kc_actions.ts names the aspects
310
+ // to fetch beneath it.
311
+ return (0, kc_actions_1.actionAspectTypes)(typeBase);
312
+ case kc_custom_types_1.CONSTRAINT_TYPE_ID:
313
+ // Custom for the same reason, and named through the same `typeBase`.
314
+ return (0, kc_constraints_1.constraintAspectTypes)(typeBase);
315
+ case kc_custom_types_1.FILE_COPY_TYPE_ID:
316
+ // Custom too: one aspect holding the file's name and text.
317
+ return [aspectType(kc_custom_types_1.FILE_COPY_TYPE_ID)];
318
+ default:
319
+ return undefined;
320
+ }
321
+ }
322
+ // Maps `items` through `fn` with at most `limit` calls in flight, returning
323
+ // results in input order (so downstream ordering stays deterministic).
324
+ async function mapConcurrent(items, limit, fn) {
325
+ const results = new Array(items.length);
326
+ let next = 0;
327
+ let failed = false;
328
+ async function worker() {
329
+ // Stop claiming new items once any worker has thrown: Promise.all rejects on
330
+ // the first failure, so fanning out more fetches is wasted work whose own
331
+ // rejections would surface as unhandled-rejection noise.
332
+ while (next < items.length && !failed) {
333
+ const i = next++;
334
+ try {
335
+ results[i] = await fn(items[i]);
336
+ }
337
+ catch (err) {
338
+ failed = true;
339
+ throw err;
340
+ }
341
+ }
342
+ }
343
+ const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
344
+ await Promise.all(workers);
345
+ return results;
346
+ }
@@ -0,0 +1,54 @@
1
+ import { Field, SemanticModel } from './ir';
2
+ /**
3
+ * Why inheritance cannot be resolved. `unknown-parent` can be a pruning
4
+ * artefact, since a profile can drop a supertype whole; a cycle or an
5
+ * ambiguously inherited field cannot, because pruning only removes things.
6
+ */
7
+ export declare class InheritanceError extends Error {
8
+ readonly kind: 'unknown-parent' | 'cycle' | 'ambiguous-field';
9
+ constructor(kind: 'unknown-parent' | 'cycle' | 'ambiguous-field', message: string);
10
+ }
11
+ export interface ResolveResult {
12
+ model: SemanticModel;
13
+ warnings: string[];
14
+ }
15
+ /**
16
+ * What a `concept` name may refer to, and the fields it declares.
17
+ *
18
+ * One lookup serves every construct that names a concept -- an action's
19
+ * `affects`, and a derived action parameter's `concept`/`field` pair -- so the
20
+ * two can never disagree about what `Order` is or which fields it has.
21
+ *
22
+ * Entities are indexed FIRST, so a name that is both an entity and a
23
+ * relationship resolves to the entity. `kind` exists to say `entity 'X'` or
24
+ * `relationship 'X'` in a message, and to pick which fields count.
25
+ *
26
+ * Only a many-to-many relationship has fields of its own: they live on the
27
+ * junction table backing it (see Association), and an enrollment's grade is as
28
+ * ordinary a field as an order's total. A plain foreign-key edge declares none.
29
+ *
30
+ * Inheritance is resolved, because a subtype's own `fields` omit everything it
31
+ * inherits. Resolution THROWS on an `extends` naming an entity the model does
32
+ * not declare, on a cycle, and on a field inherited ambiguously; a caller that
33
+ * cannot report that -- the loader, which is lenient by design -- catches it
34
+ * and works from the unresolved model instead.
35
+ */
36
+ export interface DeclaredConcept {
37
+ kind: 'entity' | 'relationship';
38
+ fields: Map<string, Field>;
39
+ }
40
+ export declare function declaredConceptFields(model: SemanticModel): Map<string, DeclaredConcept>;
41
+ /**
42
+ * Returns a clone of `model` with every entity's `extends` expanded to its full
43
+ * transitive ancestor set and its `fields` flattened to include inherited
44
+ * fields, less any a profile excludes on that entity. The input is never
45
+ * mutated. An entity with no `extends` keeps its own fields, so a model with no
46
+ * inheritance and no exclusions resolves to an equivalent model.
47
+ * `targetDialect` is the SQL that an inherited field's target text and its
48
+ * ANSI_SQL entries are read in to find their comments. It is BigQuery unless
49
+ * given, as the graph generators read the target text.
50
+ *
51
+ * Reading an inherited field's SQL needs the SQL parser, so call
52
+ * `loadSqlEngine()` first. Without it, this throws the parser's plain Error.
53
+ */
54
+ export declare function resolveInheritance(model: SemanticModel, targetDialect?: string): ResolveResult;