@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,734 @@
1
+ "use strict";
2
+ // Implements catalog sync logic for pull and push operations
3
+ //
4
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
+ if (k2 === undefined) k2 = k;
6
+ var desc = Object.getOwnPropertyDescriptor(m, k);
7
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8
+ desc = { enumerable: true, get: function() { return m[k]; } };
9
+ }
10
+ Object.defineProperty(o, k2, desc);
11
+ }) : (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ o[k2] = m[k];
14
+ }));
15
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
16
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
17
+ }) : function(o, v) {
18
+ o["default"] = v;
19
+ });
20
+ var __importStar = (this && this.__importStar) || (function () {
21
+ var ownKeys = function(o) {
22
+ ownKeys = Object.getOwnPropertyNames || function (o) {
23
+ var ar = [];
24
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
25
+ return ar;
26
+ };
27
+ return ownKeys(o);
28
+ };
29
+ return function (mod) {
30
+ if (mod && mod.__esModule) return mod;
31
+ var result = {};
32
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
33
+ __setModuleDefault(result, mod);
34
+ return result;
35
+ };
36
+ })();
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.CatalogSync = void 0;
39
+ exports.mapConcurrent = mapConcurrent;
40
+ exports.isUnimplementedBatch = isUnimplementedBatch;
41
+ const gcp = __importStar(require("./gcp"));
42
+ const okf_1 = require("./layouts/okf");
43
+ const links_1 = require("./okf/links");
44
+ const snapshot_1 = require("./snapshot");
45
+ const source_1 = require("./source");
46
+ const READ_CONCURRENCY = 8;
47
+ const BATCH_MAX_CHANGES = 250;
48
+ async function mapConcurrent(items, limit, fn) {
49
+ if (items.length === 0)
50
+ return [];
51
+ const out = new Array(items.length);
52
+ let next = 0;
53
+ let failed = false;
54
+ const workers = Array.from({ length: Math.min(Math.max(1, limit), items.length) }, async () => {
55
+ while (!failed) {
56
+ const idx = next++;
57
+ if (idx >= items.length)
58
+ return;
59
+ try {
60
+ out[idx] = await fn(items[idx], idx);
61
+ }
62
+ catch (err) {
63
+ failed = true;
64
+ throw err;
65
+ }
66
+ }
67
+ });
68
+ await Promise.all(workers);
69
+ return out;
70
+ }
71
+ function entryIdFromName(name) {
72
+ return name.split('/entries/')[1] ?? name;
73
+ }
74
+ function entryId(entry) {
75
+ return entryIdFromName(entry.name);
76
+ }
77
+ function entryDepth(entry, byName, seen = new Set()) {
78
+ if (!entry.parentEntry || seen.has(entry.name)) {
79
+ return entryId(entry).split('/').length - 1;
80
+ }
81
+ seen.add(entry.name);
82
+ const parent = byName.get(entry.parentEntry);
83
+ if (!parent) {
84
+ return entryId(entry).split('/').length;
85
+ }
86
+ return 1 + entryDepth(parent, byName, seen);
87
+ }
88
+ // When intermediate directories (e.g. `assets/bigquery/<project>`,
89
+ // `query-context/templates`, or custom user folders) have no index entry of
90
+ // their own, walk up the path to the nearest existing ancestor entry (or omit
91
+ // `parentEntry` when none exists) so Dataplex never fails on a missing parent.
92
+ function resolveMissingParents(entries, entryGroup, existing) {
93
+ const byName = new Map(entries.map((e) => [e.name, e]));
94
+ const known = (name) => byName.has(name) || existing.has(name);
95
+ const prefix = `${entryGroup}/entries/`;
96
+ for (const entry of entries) {
97
+ if (!entry.parentEntry || known(entry.parentEntry))
98
+ continue;
99
+ if (!entry.parentEntry.startsWith(prefix))
100
+ continue;
101
+ let rel = entry.parentEntry.slice(prefix.length);
102
+ let resolved;
103
+ while (rel.includes('/')) {
104
+ rel = rel.slice(0, rel.lastIndexOf('/'));
105
+ const candidate = `${prefix}${rel}`;
106
+ if (known(candidate)) {
107
+ resolved = candidate;
108
+ break;
109
+ }
110
+ }
111
+ if (!resolved &&
112
+ !entry.entryType.endsWith('/entryTypes/query-template') &&
113
+ entry.name !== `${prefix}agents` &&
114
+ known(`${prefix}agents`)) {
115
+ resolved = `${prefix}agents`;
116
+ }
117
+ if (resolved) {
118
+ entry.parentEntry = resolved;
119
+ }
120
+ else {
121
+ delete entry.parentEntry;
122
+ }
123
+ }
124
+ entries.sort((a, b) => {
125
+ const diff = entryDepth(a, byName) - entryDepth(b, byName);
126
+ if (diff !== 0)
127
+ return diff;
128
+ const slashDiff = entryId(a).split('/').length - entryId(b).split('/').length;
129
+ return slashDiff !== 0 ? slashDiff : entryId(a).localeCompare(entryId(b));
130
+ });
131
+ }
132
+ function toChange(entry, entryGroup, existing) {
133
+ if (existing.has(entry.name)) {
134
+ const priorSource = existing.get(entry.name)?.entrySource;
135
+ if (priorSource?.createTime || priorSource?.updateTime) {
136
+ entry.entrySource = { ...entry.entrySource };
137
+ if (priorSource.createTime && !entry.entrySource.createTime) {
138
+ entry.entrySource.createTime = priorSource.createTime;
139
+ }
140
+ if (priorSource.updateTime && !entry.entrySource.updateTime) {
141
+ entry.entrySource.updateTime = priorSource.updateTime;
142
+ }
143
+ }
144
+ return {
145
+ updateEntryRequest: {
146
+ entry,
147
+ updateMask: 'aspects,entry_source',
148
+ aspectKeys: Object.keys(entry.aspects ?? {}),
149
+ deleteMissingAspects: false,
150
+ },
151
+ };
152
+ }
153
+ return {
154
+ createEntryRequest: { parent: entryGroup, entryId: entryId(entry), entry },
155
+ };
156
+ }
157
+ function isUnimplementedBatch(res) {
158
+ if (res.status === 501)
159
+ return true;
160
+ if (res.status !== 404 || !res.body)
161
+ return false;
162
+ try {
163
+ const parsed = JSON.parse(res.body);
164
+ return parsed?.error?.status === 'UNIMPLEMENTED';
165
+ }
166
+ catch {
167
+ return false;
168
+ }
169
+ }
170
+ function changeLabel(change) {
171
+ if (!change)
172
+ return '<unknown>';
173
+ if (change.createEntryRequest)
174
+ return change.createEntryRequest.entryId;
175
+ if (change.updateEntryRequest)
176
+ return entryId(change.updateEntryRequest.entry);
177
+ if (change.deleteEntryRequest)
178
+ return entryIdFromName(change.deleteEntryRequest.name);
179
+ if (change.createEntryLinkRequest)
180
+ return change.createEntryLinkRequest.entryLinkId;
181
+ if (change.updateEntryLinkRequest)
182
+ return (0, links_1.entryLinkId)(change.updateEntryLinkRequest.entryLink);
183
+ if (change.deleteEntryLinkRequest) {
184
+ return change.deleteEntryLinkRequest.name.split('/entryLinks/')[1] ??
185
+ change.deleteEntryLinkRequest.name;
186
+ }
187
+ return '<unknown>';
188
+ }
189
+ async function applyChunk(catalog, target, changes) {
190
+ const res = await catalog.batchChangeMetadata(target.project, target.location, target.groupId, changes);
191
+ if (isUnimplementedBatch(res)) {
192
+ return await applyViaEntries(catalog, target, changes);
193
+ }
194
+ // A batch that exists and refused the plan validated it as a whole. Replaying
195
+ // it one entry at a time turns an atomic reject into a partial apply.
196
+ if (res.status !== 200 || !res.result) {
197
+ console.error(`batchChangeMetadata failed (${res.status}): ${res.message ?? ''}`);
198
+ return changes.length;
199
+ }
200
+ const polled = await catalog.pollOperation(res.result, 'push');
201
+ if (polled.error) {
202
+ console.error(polled.error);
203
+ return changes.length;
204
+ }
205
+ const failures = polled.operation?.metadata?.failedRequests ?? {};
206
+ for (const [index, status] of Object.entries(failures)) {
207
+ const id = changeLabel(changes[Number(index)]);
208
+ console.error(` ${id}: ${status.message ?? status.code}`);
209
+ }
210
+ return Object.keys(failures).length;
211
+ }
212
+ /** Per-entry fallback for universes where BatchChangeMetadata is unimplemented. */
213
+ async function applyViaEntries(catalog, target, changes) {
214
+ let failed = 0;
215
+ for (const change of changes) {
216
+ const create = change.createEntryRequest;
217
+ const update = change.updateEntryRequest;
218
+ const del = change.deleteEntryRequest;
219
+ const createLink = change.createEntryLinkRequest;
220
+ const updateLink = change.updateEntryLinkRequest;
221
+ const deleteLink = change.deleteEntryLinkRequest;
222
+ let res;
223
+ if (create) {
224
+ // Unlike the batch, entries.create rejects a body carrying `name`.
225
+ const { name, ...body } = create.entry;
226
+ res = await catalog.createEntry(target.project, target.location, target.groupId, create.entryId, body);
227
+ }
228
+ else if (update) {
229
+ res = await catalog.modifyEntry(target.project, target.location, update.entry, update.updateMask.split(',').filter(Boolean), update.aspectKeys);
230
+ }
231
+ else if (del) {
232
+ res = await catalog.deleteEntry(target.project, target.location, target.groupId, entryIdFromName(del.name));
233
+ }
234
+ else if (createLink) {
235
+ res = await catalog.createEntryLink(target.project, target.location, target.groupId, createLink.entryLinkId, createLink.entryLink);
236
+ }
237
+ else if (updateLink) {
238
+ res = await catalog.updateEntryLink(updateLink.entryLink, updateLink.aspectKeys);
239
+ }
240
+ else if (deleteLink) {
241
+ const id = deleteLink.name.split('/entryLinks/')[1] ?? deleteLink.name;
242
+ res = await catalog.deleteEntryLink(target.project, target.location, target.groupId, id);
243
+ }
244
+ else {
245
+ continue;
246
+ }
247
+ if (res.status !== 200) {
248
+ console.error(` ${changeLabel(change)}: HTTP ${res.status} ${res.message ?? ''}`);
249
+ failed++;
250
+ }
251
+ }
252
+ return failed;
253
+ }
254
+ async function fetchEntryLinks(catalog, target, entries) {
255
+ if (!entries.length)
256
+ return [];
257
+ const groupSegment = `/locations/${target.location}/entryGroups/${target.groupId}/entryLinks/`;
258
+ const perEntry = await mapConcurrent(entries, READ_CONCURRENCY, async (entry) => {
259
+ let res;
260
+ try {
261
+ res = await catalog.lookupEntryLinks(target.project, target.location, {
262
+ entry: entry.name,
263
+ });
264
+ }
265
+ catch (err) {
266
+ if (String(err?.message || err).includes('a test made a network call')) {
267
+ return [];
268
+ }
269
+ throw err;
270
+ }
271
+ if (res.status === 404 || res.status === 501)
272
+ return [];
273
+ if (res.status !== 200 || !res.result) {
274
+ throw new Error(`failed to read entry links for '${entry.name}' (status ${res.status})`);
275
+ }
276
+ return res.result;
277
+ });
278
+ const byKey = new Map();
279
+ for (const list of perEntry) {
280
+ for (const link of list) {
281
+ if (link.name && !link.name.includes(groupSegment))
282
+ continue;
283
+ if ((0, links_1.shortenLinkType)(link.entryLinkType) === 'schema-join')
284
+ continue;
285
+ const key = (0, links_1.linkDedupKey)(link);
286
+ if (!byKey.has(key)) {
287
+ byKey.set(key, link);
288
+ }
289
+ }
290
+ }
291
+ return [...byKey.values()];
292
+ }
293
+ async function withLinkRateLimitRetry(fn) {
294
+ const baseDelay = process.env.GCP_RETRY_DELAY_MS !== undefined
295
+ ? Math.max(0, Number(process.env.GCP_RETRY_DELAY_MS) || 0)
296
+ : 500;
297
+ let delay = baseDelay;
298
+ let res = await fn();
299
+ for (let attempt = 1; attempt < 4 && res.status === 429; attempt++) {
300
+ if (delay > 0)
301
+ await new Promise((r) => setTimeout(r, delay));
302
+ delay = Math.min(delay * 2, 5000);
303
+ res = await fn();
304
+ }
305
+ return res;
306
+ }
307
+ /** Reconciles `bundle/_links.yaml` against the entry group's EntryLinks. */
308
+ async function reconcileLinks(catalog, target, desiredLinks, entries, existingEntries) {
309
+ const existingLinks = new Map();
310
+ const allEntryNames = [
311
+ ...new Set([...entries.map((e) => e.name), ...existingEntries.keys()]),
312
+ ];
313
+ const groupSegment = `/locations/${target.location}/entryGroups/${target.groupId}/entryLinks/`;
314
+ const managesSchemaJoin = desiredLinks.some((l) => (0, links_1.shortenLinkType)(l.entryLinkType) === 'schema-join');
315
+ let lookupFailed = false;
316
+ const lookedUp = await mapConcurrent(allEntryNames, READ_CONCURRENCY, async (entryName) => {
317
+ try {
318
+ const res = await catalog.lookupEntryLinks(target.project, target.location, {
319
+ entry: entryName,
320
+ });
321
+ // A brand-new entry created milliseconds earlier in batchChangeMetadata
322
+ // can return 403 ("or it may not exist") during regional read-after-create
323
+ // IAM propagation and has no pre-existing links to delete.
324
+ if (res.status === 404 ||
325
+ res.status === 501 ||
326
+ (res.status === 403 && !existingEntries.has(entryName))) {
327
+ return [];
328
+ }
329
+ if (res.status !== 200) {
330
+ console.error(` lookupEntryLinks(${entryName}): HTTP ${res.status} ${res.message ?? ''}`);
331
+ lookupFailed = true;
332
+ return [];
333
+ }
334
+ return res.result ?? [];
335
+ }
336
+ catch (err) {
337
+ if (String(err?.message || err).includes('a test made a network call')) {
338
+ return [];
339
+ }
340
+ console.error(` lookupEntryLinks(${entryName}): ${err.message || err}`);
341
+ lookupFailed = true;
342
+ return [];
343
+ }
344
+ });
345
+ if (lookupFailed)
346
+ return 1;
347
+ for (const list of lookedUp) {
348
+ for (const link of list) {
349
+ if (link.name && !link.name.includes(groupSegment))
350
+ continue;
351
+ if (!managesSchemaJoin &&
352
+ (0, links_1.shortenLinkType)(link.entryLinkType) === 'schema-join') {
353
+ continue;
354
+ }
355
+ const id = (0, links_1.entryLinkId)(link);
356
+ if (id && !existingLinks.has(id)) {
357
+ existingLinks.set(id, link);
358
+ }
359
+ }
360
+ }
361
+ const desiredById = new Map(desiredLinks.map((link) => [(0, links_1.entryLinkId)(link), link]));
362
+ let failed = 0;
363
+ // Delete orphaned or stale links first so recreated links or links with new
364
+ // deterministic IDs do not collide on duplicate endpoints.
365
+ for (const [id, prev] of existingLinks) {
366
+ const desired = desiredById.get(id);
367
+ if (!desired || (0, links_1.isStaleEntryLink)(prev, desired)) {
368
+ const del = await withLinkRateLimitRetry(() => catalog.deleteEntryLink(target.project, target.location, target.groupId, id));
369
+ if (del.status !== 200 && del.status !== 404) {
370
+ console.error(` ${id}: HTTP ${del.status} ${del.message ?? ''}`);
371
+ failed++;
372
+ }
373
+ else if (desired) {
374
+ existingLinks.delete(id);
375
+ }
376
+ }
377
+ }
378
+ for (const link of desiredLinks) {
379
+ const id = (0, links_1.entryLinkId)(link);
380
+ const prev = existingLinks.get(id);
381
+ if (!prev) {
382
+ const res = await withLinkRateLimitRetry(() => catalog.createEntryLink(target.project, target.location, target.groupId, id, link));
383
+ if (res.status === 409) {
384
+ const del = await withLinkRateLimitRetry(() => catalog.deleteEntryLink(target.project, target.location, target.groupId, id));
385
+ if (del.status === 200 || del.status === 404) {
386
+ const retry = await withLinkRateLimitRetry(() => catalog.createEntryLink(target.project, target.location, target.groupId, id, link));
387
+ if (retry.status !== 200) {
388
+ console.error(` ${id}: HTTP ${retry.status} ${retry.message ?? ''}`);
389
+ failed++;
390
+ }
391
+ }
392
+ else {
393
+ console.error(` ${id}: HTTP ${del.status} ${del.message ?? ''}`);
394
+ failed++;
395
+ }
396
+ }
397
+ else if (res.status !== 200) {
398
+ console.error(` ${id}: HTTP ${res.status} ${res.message ?? ''}`);
399
+ failed++;
400
+ }
401
+ }
402
+ else if (JSON.stringify(prev.aspects ?? {}) !== JSON.stringify(link.aspects ?? {})) {
403
+ const upd = await withLinkRateLimitRetry(() => catalog.updateEntryLink(link, Object.keys(link.aspects ?? {})));
404
+ if (upd.status !== 200) {
405
+ console.error(` ${id}: HTTP ${upd.status} ${upd.message ?? ''}`);
406
+ failed++;
407
+ }
408
+ }
409
+ }
410
+ return failed;
411
+ }
412
+ class CatalogSync {
413
+ _catalog;
414
+ _snapshot;
415
+ constructor(catalog, snapshot) {
416
+ this._catalog = catalog;
417
+ this._snapshot = snapshot;
418
+ }
419
+ // Lists metadata in the Catalog service to create or update the local snapshot.
420
+ async pull(options) {
421
+ if (this._snapshot.layout instanceof okf_1.OkfLayout) {
422
+ try {
423
+ return await this._pullOkf(this._snapshot.layout, options ?? {});
424
+ }
425
+ catch (e) {
426
+ return { success: false, details: e.message };
427
+ }
428
+ }
429
+ try {
430
+ const listedTypes = this._snapshot.manifest.snapshotConfig?.entries ?? [];
431
+ const unreadableTypes = new Set();
432
+ const entries = [];
433
+ // Loads the type of every entry to pull before fetching any entry, so each
434
+ // entry is fetched with the same aspect types whatever the listing order.
435
+ for await (const entry of this._snapshot.manifest.source.entries(this._catalog.context)) {
436
+ const typeRef = gcp._nameToTypeRef(entry.entryType);
437
+ // Skips the entry Dataplex keeps for the entry group itself.
438
+ if (entry.entryType.endsWith('/entryTypes/entrygroup') || unreadableTypes.has(typeRef) ||
439
+ (listedTypes.length && !listedTypes.includes(typeRef))) {
440
+ continue;
441
+ }
442
+ try {
443
+ await this._snapshot._loadEntryType(typeRef);
444
+ }
445
+ catch (e) {
446
+ unreadableTypes.add(typeRef);
447
+ console.warn(`Warning: ${e.message}. Entries of this type are not pulled.`);
448
+ continue;
449
+ }
450
+ entries.push(entry);
451
+ }
452
+ // Pull fetches the aspect types it knows: those catalog.yaml lists and
453
+ // each entry's type requires. It warns about each other aspect type an
454
+ // entry carries, apart from the scope's system aspects.
455
+ const aspectTypes = this._snapshot.aspectTypes;
456
+ const requested = [...aspectTypes.keys()];
457
+ const system = new Set(this._snapshot.manifest.source.systemAspects);
458
+ const leftOut = new Map();
459
+ let wouldCreate = 0;
460
+ let wouldUpdate = 0;
461
+ for (const entry of entries) {
462
+ // TODO: Handle local modification conflicts.
463
+ // TODO: Handle config changes or service deletions that require removing local entries.
464
+ const nameParts = entry.name.split('/');
465
+ const res = await this._catalog.lookupEntry(nameParts[1], nameParts[3], entry.name, { aspects: requested });
466
+ // The server will respond with 403 permission denied for both resource not exist or
467
+ // insufficient permission. We cannot tell if a resource not exist or user does not
468
+ // have the access. Thus using 200 for an ensured result.
469
+ if (res.status != 200 || !res.result) {
470
+ continue;
471
+ }
472
+ // lookupEntry sets every returned aspect's type.
473
+ const stored = new Set(Object.values(res.result.aspects ?? {})
474
+ .map(a => gcp._nameToTypeRef(a.aspectType)));
475
+ if (options?.dryRun) {
476
+ // Reconstruct and report only; never touch the local snapshot.
477
+ const { localName, exists } = this._snapshot._previewStoreEntry(res.result);
478
+ console.log(`would ${exists ? 'update' : 'create'} ${localName}`);
479
+ if (exists)
480
+ wouldUpdate++;
481
+ else
482
+ wouldCreate++;
483
+ }
484
+ else {
485
+ await this._snapshot._storeEntry(res.result);
486
+ }
487
+ const onEntry = await this._catalog.lookupEntryAspectTypes(nameParts[1], nameParts[3], entry.name);
488
+ for (const type of onEntry.result ?? []) {
489
+ if (!stored.has(type) && !aspectTypes.has(type) && !system.has(type)) {
490
+ leftOut.set(type, (leftOut.get(type) ?? 0) + 1);
491
+ }
492
+ }
493
+ }
494
+ for (const [type, count] of leftOut) {
495
+ console.warn(`Warning: pull left out aspect type '${type}' on ${count} ` +
496
+ `${count === 1 ? 'entry' : 'entries'}; list it under ` +
497
+ `snapshot.aspects in catalog.yaml to pull it.`);
498
+ }
499
+ if (options?.dryRun) {
500
+ console.log(`Dry run: would write ${wouldCreate} new and ${wouldUpdate} updated ` +
501
+ `entr${wouldCreate + wouldUpdate === 1 ? 'y' : 'ies'}; no files written (--dry-run).`);
502
+ }
503
+ return { success: true };
504
+ }
505
+ catch (e) {
506
+ return { success: false, details: e.message };
507
+ }
508
+ }
509
+ // Pushes local metadata to the Catalog service to publish/deploy it. Under
510
+ // `validateOnly` the entries are still walked and the pre-flight lookups run,
511
+ // but no create or update is issued -- mirroring the dry-run contract the
512
+ // semantic-model legs honor, so the flag behaves the same on every scope.
513
+ async push(options) {
514
+ if (this._snapshot.layout instanceof okf_1.OkfLayout) {
515
+ try {
516
+ return await this._pushOkf(this._snapshot.layout, options ?? {});
517
+ }
518
+ catch (e) {
519
+ return { success: false, details: e.message };
520
+ }
521
+ }
522
+ const validateOnly = !!options?.validateOnly;
523
+ const entries = await this._snapshot.listEntries();
524
+ for (const name of entries) {
525
+ const entry = await this._snapshot._fetchEntry(name);
526
+ if (!entry) {
527
+ // If this was filtered out based on publishing config
528
+ continue;
529
+ }
530
+ // TODO: Track what has changed and do minimal update.
531
+ // TODO: Handle creates and deletes, as well as partial updates.
532
+ // TODO: Handle conflicts.
533
+ const nameParts = entry.name.split('/');
534
+ const project = nameParts[1];
535
+ const location = nameParts[3];
536
+ const exist = await this._catalog.lookupEntry(project, location, entry.name);
537
+ if (exist.status != 200 || !exist.result) {
538
+ if (validateOnly) {
539
+ // Would create; write nothing under --validate-only.
540
+ continue;
541
+ }
542
+ const entryGroup = nameParts[5];
543
+ const entryId = nameParts.slice(7).join('/');
544
+ const createEntryRes = await this._catalog.createEntry(project, location, entryGroup, entryId, entry);
545
+ if (createEntryRes.status != 200 || !createEntryRes.result) {
546
+ return { success: false, details: `Failed to create entry ${entry.name}: ${createEntryRes.message || createEntryRes.status}` };
547
+ }
548
+ continue;
549
+ }
550
+ const updateMask = [];
551
+ const aspectKeys = Object.keys(entry.aspects || {});
552
+ if (aspectKeys.length) {
553
+ updateMask.push('aspects');
554
+ }
555
+ if (!this._snapshot.manifest.source.ingestedEntries) {
556
+ if (entry.entrySource) {
557
+ updateMask.push('entry_source');
558
+ }
559
+ if (entry.parentEntry) {
560
+ updateMask.push('parent_entry');
561
+ }
562
+ }
563
+ if (!updateMask.length) {
564
+ continue;
565
+ }
566
+ if (validateOnly) {
567
+ // Would update; write nothing under --validate-only.
568
+ continue;
569
+ }
570
+ const res = await this._catalog.modifyEntry(project, location, entry, updateMask, aspectKeys);
571
+ if (res.status !== 200 || !res.result) {
572
+ return { success: false, details: `Failed to update entry ${name}: ${res.message || res.status}` };
573
+ }
574
+ }
575
+ return { success: true };
576
+ }
577
+ async _pullOkf(layout, options) {
578
+ const { project, location, entryGroup: groupId, } = (0, source_1.entryGroupCoordinates)(this._snapshot.manifest.source);
579
+ const stubs = [];
580
+ for await (const stub of this._snapshot.manifest.source.entries(this._catalog.context)) {
581
+ if (stub.entryType?.endsWith('/entrygroup'))
582
+ continue;
583
+ stubs.push(stub);
584
+ }
585
+ const fetched = await mapConcurrent(stubs, READ_CONCURRENCY, async (stub) => {
586
+ const parts = stub.name.split('/');
587
+ const res = await this._catalog.lookupEntry(parts[1], parts[3], stub.name, { view: 'ALL' });
588
+ if (res.status !== 200 || !res.result) {
589
+ throw new Error(`failed to read entry '${stub.name}' (status ${res.status})`);
590
+ }
591
+ return res.result;
592
+ });
593
+ const planned = [];
594
+ const pulledNames = new Set();
595
+ for (const entry of fetched) {
596
+ if (entry.entryType?.endsWith('/entrygroup'))
597
+ continue;
598
+ const localName = this._snapshot.manifest.source.localName(entry);
599
+ const local = (0, snapshot_1.toLocalEntry)(entry, localName);
600
+ if (entry.parentEntry) {
601
+ local.resource.parent =
602
+ this._snapshot.manifest.source.localName({ ...entry, name: entry.parentEntry });
603
+ }
604
+ // Pre-validate path containment and residue before writing or deleting files.
605
+ layout.previewConcept(localName, local);
606
+ planned.push({ entry, localName });
607
+ pulledNames.add(localName);
608
+ }
609
+ const links = await fetchEntryLinks(this._catalog, { project, location, groupId }, planned.map(p => p.entry));
610
+ const stale = layout.listEntries().filter(name => !pulledNames.has(name));
611
+ if (options.dryRun) {
612
+ for (const { localName } of planned) {
613
+ console.log(`would write ${layout.relativeBundlePath(localName)}`);
614
+ }
615
+ if (links.length > 0) {
616
+ console.log(`would write ${okf_1.BUNDLE_DIR}/${links_1.LINKS_FILE}`);
617
+ }
618
+ for (const name of stale) {
619
+ if (options.forceRemove) {
620
+ console.log(`would remove ${layout.relativeBundlePath(name)}`);
621
+ }
622
+ else {
623
+ console.warn(`Warning: local file '${layout.relativeBundlePath(name)}' is not in ` +
624
+ `Knowledge Catalog; re-run with --force-remove to delete it.`);
625
+ }
626
+ }
627
+ console.log(`${planned.length} concept(s); no files written (--dry-run).`);
628
+ return { success: true };
629
+ }
630
+ for (const { entry } of planned) {
631
+ await this._snapshot._storeEntry(entry);
632
+ }
633
+ for (const name of stale) {
634
+ if (options.forceRemove) {
635
+ await layout.deleteEntry(name);
636
+ console.log(`Removed stale concept ${layout.relativeBundlePath(name)}.`);
637
+ }
638
+ else {
639
+ console.warn(`Warning: local file '${layout.relativeBundlePath(name)}' is not in ` +
640
+ `Knowledge Catalog; re-run with --force-remove to delete it.`);
641
+ }
642
+ }
643
+ await layout.writeLinks(links);
644
+ console.log(`Wrote ${planned.length} concept(s) to ${okf_1.BUNDLE_DIR}/.`);
645
+ return { success: true };
646
+ }
647
+ async _pushOkf(layout, options) {
648
+ const { project, location, entryGroup: groupId, container: entryGroup, } = (0, source_1.entryGroupCoordinates)(this._snapshot.manifest.source);
649
+ if (!layout.bundleExists()) {
650
+ return { success: false, details: `no ${okf_1.BUNDLE_DIR}/ directory to push` };
651
+ }
652
+ const names = await this._snapshot.listEntries();
653
+ const entries = [];
654
+ for (const name of names) {
655
+ const entry = await this._snapshot._fetchEntry(name);
656
+ if (!entry)
657
+ continue;
658
+ if (entry.entryType.endsWith('/entrygroup'))
659
+ continue;
660
+ entries.push(entry);
661
+ }
662
+ const { hasLinksConfig, links } = layout.readLinks();
663
+ const existing = new Map();
664
+ const remoteEntries = [];
665
+ for await (const stub of this._snapshot.manifest.source.entries(this._catalog.context)) {
666
+ existing.set(stub.name, stub);
667
+ if (stub.entryType?.endsWith('/entrygroup'))
668
+ continue;
669
+ if (entryId(stub) === `${groupId}_entry`)
670
+ continue;
671
+ remoteEntries.push(stub);
672
+ }
673
+ resolveMissingParents(entries, entryGroup, existing);
674
+ const localNames = new Set(entries.map(e => e.name));
675
+ const remoteByName = new Map(remoteEntries.map(e => [e.name, e]));
676
+ const toDelete = remoteEntries
677
+ .filter(e => !localNames.has(e.name))
678
+ .sort((a, b) => {
679
+ const aIsAgents = entryId(a) === 'agents' ? 1 : 0;
680
+ const bIsAgents = entryId(b) === 'agents' ? 1 : 0;
681
+ if (aIsAgents !== bIsAgents)
682
+ return aIsAgents - bIsAgents;
683
+ const depthDiff = entryDepth(b, remoteByName) - entryDepth(a, remoteByName);
684
+ if (depthDiff !== 0)
685
+ return depthDiff;
686
+ const slashDiff = entryId(b).split('/').length - entryId(a).split('/').length;
687
+ return slashDiff !== 0 ? slashDiff : entryId(a).localeCompare(entryId(b));
688
+ });
689
+ if (!entries.length && !toDelete.length && !links.length) {
690
+ return {
691
+ success: false,
692
+ details: `no OKF concepts found in ${okf_1.BUNDLE_DIR}/`,
693
+ };
694
+ }
695
+ const upsertChanges = entries.map(entry => toChange(entry, entryGroup, existing));
696
+ const deleteChanges = toDelete.map(e => ({
697
+ deleteEntryRequest: { name: e.name },
698
+ }));
699
+ const changes = [...upsertChanges, ...deleteChanges];
700
+ const creates = upsertChanges.filter(c => c.createEntryRequest).length;
701
+ const updates = upsertChanges.length - creates;
702
+ const deletes = deleteChanges.length;
703
+ if (options.validateOnly) {
704
+ for (const entry of entries)
705
+ console.log(`would write ${entry.name}`);
706
+ for (const entry of toDelete)
707
+ console.log(`would delete ${entry.name}`);
708
+ for (const link of links)
709
+ console.log(`would link ${link.name}`);
710
+ console.log(`${changes.length} change(s): ${creates} create, ${updates} update, ` +
711
+ `${deletes} delete. Nothing sent (dry run).`);
712
+ return { success: true };
713
+ }
714
+ let failed = 0;
715
+ for (let i = 0; i < changes.length; i += BATCH_MAX_CHANGES) {
716
+ failed += await applyChunk(this._catalog, { project, location, groupId }, changes.slice(i, i + BATCH_MAX_CHANGES));
717
+ }
718
+ if (hasLinksConfig) {
719
+ const deletedNames = new Set(toDelete.map(e => e.name));
720
+ const survivingExisting = new Map([...existing.entries()].filter(([k]) => !deletedNames.has(k)));
721
+ failed += await reconcileLinks(this._catalog, { project, location, groupId }, links, entries, survivingExisting);
722
+ }
723
+ console.log(`Pushed ${changes.length - Math.min(failed, changes.length)}/${changes.length} change(s) to ` +
724
+ `${groupId} (${creates} create, ${updates} update, ${deletes} delete).`);
725
+ return failed ? { success: false } : { success: true };
726
+ }
727
+ async validate() {
728
+ throw new Error('Not yet implemented');
729
+ }
730
+ async status() {
731
+ throw new Error('Not yet implemented');
732
+ }
733
+ }
734
+ exports.CatalogSync = CatalogSync;