@google-cloud/kcmd 0.1.0 → 0.2.1

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 (67) hide show
  1. package/README.md +18 -20
  2. package/build/ts/kcmd/cli.js +6164 -5631
  3. package/build/ts/kcmd/gcp/biglake.d.ts +2 -0
  4. package/build/ts/kcmd/gcp/biglake.js +13 -4
  5. package/build/ts/kcmd/gcp/spanner.d.ts +10 -0
  6. package/build/ts/kcmd/gcp/spanner.js +6 -3
  7. package/build/ts/kcmd/layouts/okf.js +49 -7
  8. package/build/ts/kcmd/layouts/semantic-model.d.ts +13 -0
  9. package/build/ts/kcmd/layouts/semantic-model.js +129 -50
  10. package/build/ts/kcmd/okf/concept.d.ts +1 -1
  11. package/build/ts/kcmd/okf/concept.js +6 -0
  12. package/build/ts/kcmd/okf/concept_to_entry.d.ts +2 -0
  13. package/build/ts/kcmd/okf/concept_to_entry.js +29 -2
  14. package/build/ts/kcmd/okf/init_okf_workspace.js +7 -9
  15. package/build/ts/kcmd/okf/okf_metadata.js +17 -0
  16. package/build/ts/kcmd/semantic/bigquery.js +139 -195
  17. package/build/ts/kcmd/semantic/binding.js +14 -19
  18. package/build/ts/kcmd/semantic/deploy_bigquery.js +6 -13
  19. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +10 -35
  20. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +105 -162
  21. package/build/ts/kcmd/semantic/deploy_spanner.js +7 -14
  22. package/build/ts/kcmd/semantic/deployment_target.d.ts +1 -4
  23. package/build/ts/kcmd/semantic/deployment_target.js +6 -62
  24. package/build/ts/kcmd/semantic/graph_ddl.d.ts +10 -0
  25. package/build/ts/kcmd/semantic/graph_ddl.js +152 -59
  26. package/build/ts/kcmd/semantic/import_model.d.ts +26 -4
  27. package/build/ts/kcmd/semantic/import_model.js +531 -111
  28. package/build/ts/kcmd/semantic/ir.d.ts +2 -4
  29. package/build/ts/kcmd/semantic/kc_converter.d.ts +7 -1
  30. package/build/ts/kcmd/semantic/kc_converter.js +32 -27
  31. package/build/ts/kcmd/semantic/kc_custom_types.d.ts +5 -12
  32. package/build/ts/kcmd/semantic/kc_custom_types.js +43 -114
  33. package/build/ts/kcmd/semantic/kc_ids.d.ts +4 -5
  34. package/build/ts/kcmd/semantic/kc_ids.js +4 -7
  35. package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +0 -3
  36. package/build/ts/kcmd/semantic/knowledge_catalog.js +3 -86
  37. package/build/ts/kcmd/semantic/loader.d.ts +7 -11
  38. package/build/ts/kcmd/semantic/loader.js +282 -171
  39. package/build/ts/kcmd/semantic/osi_converter.js +14 -65
  40. package/build/ts/kcmd/semantic/parameters.d.ts +1 -0
  41. package/build/ts/kcmd/semantic/parameters.js +22 -3
  42. package/build/ts/kcmd/semantic/pull_kc.d.ts +1 -5
  43. package/build/ts/kcmd/semantic/pull_kc.js +38 -112
  44. package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +7 -2
  45. package/build/ts/kcmd/semantic/resolve_inheritance.js +37 -21
  46. package/build/ts/kcmd/semantic/resolve_profiles.d.ts +51 -10
  47. package/build/ts/kcmd/semantic/resolve_profiles.js +213 -32
  48. package/build/ts/kcmd/semantic/runtime/runtime.d.ts +4 -12
  49. package/build/ts/kcmd/semantic/runtime/runtime.js +4 -21
  50. package/build/ts/kcmd/semantic/runtime/store.d.ts +14 -19
  51. package/build/ts/kcmd/semantic/runtime/store.js +21 -37
  52. package/build/ts/kcmd/semantic/source_dialect.d.ts +9 -2
  53. package/build/ts/kcmd/semantic/source_dialect.js +83 -33
  54. package/build/ts/kcmd/semantic/source_entries.d.ts +1 -1
  55. package/build/ts/kcmd/semantic/source_entries.js +95 -36
  56. package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +13 -7
  57. package/build/ts/kcmd/semantic/sql_expr_utils.js +240 -67
  58. package/build/ts/kcmd/semantic/sql_parser.d.ts +5 -2
  59. package/build/ts/kcmd/semantic/sql_parser.js +22 -0
  60. package/build/ts/kcmd/semantic/transpile.d.ts +12 -10
  61. package/build/ts/kcmd/semantic/transpile.js +79 -61
  62. package/build/ts/kcmd/semantic/validate.d.ts +37 -2
  63. package/build/ts/kcmd/semantic/validate.js +467 -126
  64. package/build/ts/kcmd/snapshot.d.ts +4 -0
  65. package/build/ts/kcmd/snapshot.js +7 -0
  66. package/build/ts/kcmd/sync.js +44 -7
  67. package/package.json +1 -1
@@ -33,10 +33,8 @@ export interface CustomExtension {
33
33
  * (edges), with model-level metrics defined over them.
34
34
  *
35
35
  * The IR is primarily semantics. The loader reads a model's named
36
- * `deployments:` list onto `deployments`. In the Google flavor it also folds
37
- * their target URIs into a model-level GOOGLE `custom_extensions` block for the
38
- * consumers that read it. A vanilla model lists its deployments in its GOOGLE
39
- * block, which the loader reads and does not fold.
36
+ * `deployments:` list onto `deployments` (from `deployments:` in the Google
37
+ * flavor or from the model-level GOOGLE `custom_extensions` block in vanilla).
40
38
  */
41
39
  export interface SemanticModel {
42
40
  name: string;
@@ -6,6 +6,11 @@ export interface ReadResult {
6
6
  warnings: string[];
7
7
  secondGeneration?: boolean;
8
8
  overviews?: Map<string, OverviewSidecar>;
9
+ firstGeneration?: FirstGenerationModel[];
10
+ }
11
+ export interface FirstGenerationModel {
12
+ name: string;
13
+ entries: Entry[];
9
14
  }
10
15
  /**
11
16
  * Reconstructs the Semantic Model IR from Knowledge Catalog entries.
@@ -22,7 +27,8 @@ export interface ReadResult {
22
27
  *
23
28
  * With `opts.v2Aspects` set, a model whose `semantic-model` aspect has
24
29
  * `specVersion` is rebuilt from the second-generation types (see
25
- * readModelV2), which reads no entry link.
30
+ * readModelV2), which reads no entry link. A model without it is returned in
31
+ * `firstGeneration` instead of being rebuilt.
26
32
  */
27
33
  export declare function modelsFromCatalogResources(entries: Entry[], entryLinks?: EntryLink[], opts?: {
28
34
  v2Aspects?: boolean;
@@ -31,9 +31,8 @@
31
31
  // `guidelines` aspect, on the model/entity/metric), each metric's attach entity
32
32
  // (re-derived from its expression, as the loader does, when the catalog holds
33
33
  // one, else the value the emitter persisted), the model's deployment targets
34
- // (from the semantic-model aspect, back into `deployments` and the GOOGLE
35
- // `custom_extensions` block), and 1:1 / 1:N relationships (from the
36
- // `schema-join` entry links a pull fetched -- see
34
+ // (from the semantic-model aspect, back into `deployments`), and 1:1 / 1:N
35
+ // relationships (from the `schema-join` entry links a pull fetched -- see
37
36
  // `modelsFromCatalogResources`'s `entryLinks` argument).
38
37
  // The per-field `semantics` block -- field/metric expressions and the DIMENSION
39
38
  // role
@@ -102,7 +101,8 @@ function warnDanglingGuards(actions, constraints, modelName, warnings) {
102
101
  *
103
102
  * With `opts.v2Aspects` set, a model whose `semantic-model` aspect has
104
103
  * `specVersion` is rebuilt from the second-generation types (see
105
- * readModelV2), which reads no entry link.
104
+ * readModelV2), which reads no entry link. A model without it is returned in
105
+ * `firstGeneration` instead of being rebuilt.
106
106
  */
107
107
  function modelsFromCatalogResources(entries, entryLinks = [], opts = {}) {
108
108
  const warnings = [];
@@ -128,11 +128,24 @@ function modelsFromCatalogResources(entries, entryLinks = [], opts = {}) {
128
128
  (soleAnchor === anchorName && !anchorNames.has(e.parentEntry ?? '')));
129
129
  let secondGeneration = false;
130
130
  const overviews = new Map();
131
- const models = anchors.map(anchor => {
132
- const name = anchor.entrySource?.displayName ?? idOf(anchor.name);
131
+ const nameOf = (anchor) => anchor.entrySource?.displayName ?? idOf(anchor.name);
132
+ // With the second-generation types, a model whose model entry has no
133
+ // specVersion was pushed with the first-generation types. It is left out
134
+ // rather than rebuilt from them.
135
+ const isFirstGeneration = (anchor) => !!opts.v2Aspects &&
136
+ aspectData(anchor, 'semantic-model').specVersion === undefined;
137
+ const firstGeneration = anchors.filter(isFirstGeneration).map(anchor => ({
138
+ name: nameOf(anchor),
139
+ entries: [
140
+ anchor,
141
+ ...[entityEntries, metricEntries, actionEntries, constraintEntries]
142
+ .flatMap(pool => childrenOf(anchor.name, pool)),
143
+ ],
144
+ }));
145
+ const models = anchors.filter(a => !isFirstGeneration(a)).map(anchor => {
146
+ const name = nameOf(anchor);
133
147
  const children = (pool) => childrenOf(anchor.name, pool);
134
- if (opts.v2Aspects &&
135
- aspectData(anchor, 'semantic-model').specVersion !== undefined) {
148
+ if (opts.v2Aspects) {
136
149
  secondGeneration = true;
137
150
  const [entities, metrics, relationships] = [entityEntries, metricEntries, relationshipEntries].map(children);
138
151
  overviewSidecars([anchor, ...entities, ...metrics, ...relationships])
@@ -162,13 +175,11 @@ function modelsFromCatalogResources(entries, entryLinks = [], opts = {}) {
162
175
  if (description !== undefined)
163
176
  model.description = description;
164
177
  readActionsAndConstraints(model, children(actionEntries), children(constraintEntries), warnings);
165
- // Deployment targets ride back in both the GOOGLE custom_extensions block
166
- // and as synthesized `deployments` entries (`default` when one,
167
- // `deployment_1`, `deployment_2`, ... when several).
168
- const targets = readDeploymentTargets(anchor, name, warnings);
169
- if (targets) {
170
- model.customExtensions = [targets.extension];
171
- model.deployments = targets.deployments;
178
+ // Deployment targets ride back as synthesized `deployments` entries
179
+ // (`default` when one, `deployment_1`, `deployment_2`, ... when several).
180
+ const deployments = readDeploymentTargets(anchor, name, warnings);
181
+ if (deployments) {
182
+ model.deployments = deployments;
172
183
  }
173
184
  const ai = readAiContext(anchor);
174
185
  if (ai)
@@ -189,6 +200,7 @@ function modelsFromCatalogResources(entries, entryLinks = [], opts = {}) {
189
200
  models,
190
201
  warnings: [...new Set(warnings)],
191
202
  ...(secondGeneration ? { secondGeneration, overviews } : {}),
203
+ ...(firstGeneration.length ? { firstGeneration } : {}),
192
204
  };
193
205
  }
194
206
  // Reads a model's action and constraint entries onto `model`.
@@ -664,10 +676,10 @@ function aspectDataOf(aspects, type, path = '') {
664
676
  return {};
665
677
  }
666
678
  // Recovers the model's deployment targets from its semantic-model aspect back
667
- // into both the GOOGLE custom_extension shim and a `deployments` list (`default`
668
- // when there is one target, `deployment_1`, `deployment_2`, ... when there are
669
- // several), warning that the original deployment names were not recovered.
670
- // Returns undefined when the model has none.
679
+ // into a `deployments` list (`default` when there is one target,
680
+ // `deployment_1`, `deployment_2`, ... when there are several), warning that the
681
+ // original deployment names were not recovered. Returns undefined when the
682
+ // model has none.
671
683
  function readDeploymentTargets(anchor, modelName, warnings) {
672
684
  const deployments = (0, deployment_target_1.unnamedDeployments)(asArray(aspectData(anchor, 'semantic-model').deploymentTargets)
673
685
  .filter((t) => typeof t === 'string'));
@@ -680,16 +692,9 @@ function readDeploymentTargets(anchor, modelName, warnings) {
680
692
  if (problem !== undefined)
681
693
  warnings.push(problem);
682
694
  }
683
- const targets = deployments.map(d => d.target);
684
695
  warnings.push(`model '${modelName}': deployment names were not recovered (the ` +
685
696
  `catalog stores target URIs only); using ${deployments.map(d => `'${d.name}'`).join(', ')}`);
686
- return {
687
- extension: {
688
- vendorName: 'GOOGLE',
689
- data: JSON.stringify({ deploymentTargets: targets }),
690
- },
691
- deployments,
692
- };
697
+ return deployments;
693
698
  }
694
699
  // Inverts schemaJoinAspectData (knowledge_catalog.ts): each schema-join link
695
700
  // whose two endpoints are both this model's entity entries becomes one
@@ -4,21 +4,15 @@ export interface CustomType {
4
4
  id: string;
5
5
  entryType: Omit<EntryType, 'name' | 'requiredAspects'>;
6
6
  aspectType: Omit<AspectType, 'name'>;
7
- required?: boolean;
8
7
  }
9
8
  export declare function aiContextField(index: number): Record<string, any>;
10
9
  export declare function aiContextAspectValue(ai: AiContext | undefined): Record<string, any> | undefined;
11
10
  export declare function aiContextFromAspect(value: any): AiContext | undefined;
12
11
  export declare const ACTION_TYPE_ID = "semantic-action";
13
12
  export declare const CONSTRAINT_TYPE_ID = "semantic-constraint";
14
- export declare const FILE_COPY_TYPE_ID = "semantic-model-file";
15
- export declare const PUSH_ID_LABEL = "kcmd-push-id";
16
- export interface FileCopy {
17
- fileName: string;
18
- content: string;
19
- pushId?: string;
20
- pushFileCount?: number;
21
- }
13
+ export declare const PUSH_STATE_LABEL = "kcmd-push-state";
14
+ export declare const PUSH_WRITING = "writing";
15
+ export declare const PUSH_FINISHED = "finished";
22
16
  export declare const CUSTOM_TYPES: readonly CustomType[];
23
17
  export declare function isCustomType(id: string): boolean;
24
18
  /**
@@ -56,9 +50,8 @@ export interface ProvisionResult {
56
50
  * template lacks fails with an opaque parsing error. Dataplex rejects a
57
51
  * backwards-incompatible change, so the patch only ever adds appended fields.
58
52
  *
59
- * Stops at the first hard failure, which includes a permission refusal for a
60
- * required type that does not exist. A refusal to patch a type that exists is
61
- * a warning. A refusal for an optional type does NOT stop it: the refusals
53
+ * Stops at the first hard failure. A refusal to patch a type that exists is
54
+ * a warning. A refusal to create a type does NOT stop it: the refusals
62
55
  * differ per type and per operation -- patching a type that already exists
63
56
  * needs `update`, creating one that does not needs `create` -- so a caller
64
57
  * refused one type may well be allowed the next, and giving up early would
@@ -37,7 +37,7 @@
37
37
  // removing an existing field breaks the update for every project that already
38
38
  // provisioned the type.
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
- exports.CUSTOM_TYPES = exports.PUSH_ID_LABEL = exports.FILE_COPY_TYPE_ID = exports.CONSTRAINT_TYPE_ID = exports.ACTION_TYPE_ID = void 0;
40
+ exports.CUSTOM_TYPES = exports.PUSH_FINISHED = exports.PUSH_WRITING = exports.PUSH_STATE_LABEL = exports.CONSTRAINT_TYPE_ID = exports.ACTION_TYPE_ID = void 0;
41
41
  exports.aiContextField = aiContextField;
42
42
  exports.aiContextAspectValue = aiContextAspectValue;
43
43
  exports.aiContextFromAspect = aiContextFromAspect;
@@ -549,67 +549,12 @@ const CONSTRAINT_ASPECT_TYPE = {
549
549
  ],
550
550
  },
551
551
  };
552
- // The id of the file-copy type. A file copy holds the text of one model or
553
- // profile file as push read it, so a pull can return the file unchanged.
554
- exports.FILE_COPY_TYPE_ID = 'semantic-model-file';
555
- // The entrySource label a push puts on the model entry when it stores file
556
- // copies. Its value is the push id those copies carry. A model entry whose
557
- // label names a push the catalog holds no copies of was last written by a
558
- // push that stopped before storing them. kcmd owns this label and no other.
559
- exports.PUSH_ID_LABEL = 'kcmd-push-id';
560
- // The aspect that carries a file copy.
561
- const FILE_COPY_ASPECT_TYPE = {
562
- displayName: 'Semantic Model File',
563
- description: 'The text of one semantic model file or profile file, exactly as ' +
564
- 'pushed.',
565
- metadataTemplate: {
566
- name: exports.FILE_COPY_TYPE_ID,
567
- type: 'record',
568
- recordFields: [
569
- {
570
- index: 1,
571
- name: 'fileName',
572
- type: 'string',
573
- constraints: { required: true },
574
- annotations: {
575
- displayName: 'File Name',
576
- description: 'The file\'s path relative to the entry group directory, for ' +
577
- 'example `retail.profile.prod.yaml`.',
578
- },
579
- },
580
- {
581
- index: 2,
582
- name: 'content',
583
- type: 'string',
584
- constraints: { required: true },
585
- annotations: {
586
- displayName: 'Content',
587
- description: 'The file\'s text, exactly as read.',
588
- },
589
- },
590
- {
591
- index: 3,
592
- name: 'pushId',
593
- type: 'string',
594
- annotations: {
595
- displayName: 'Push ID',
596
- description: 'The id of the push that stored this copy. Every copy one push ' +
597
- 'stores carries the same id.',
598
- },
599
- },
600
- {
601
- index: 4,
602
- name: 'pushFileCount',
603
- type: 'int',
604
- annotations: {
605
- displayName: 'Push File Count',
606
- description: 'How many file copies the push that stored this one ' +
607
- 'stored in all.',
608
- },
609
- },
610
- ],
611
- },
612
- };
552
+ // The entrySource label every push puts on the model entry: PUSH_WRITING with
553
+ // the model entry's write and PUSH_FINISHED as the push's last write. kcmd
554
+ // owns this label and no other.
555
+ exports.PUSH_STATE_LABEL = 'kcmd-push-state';
556
+ exports.PUSH_WRITING = 'writing';
557
+ exports.PUSH_FINISHED = 'finished';
613
558
  // Every type kcmd provisions. This list is the whole of what is custom.
614
559
  exports.CUSTOM_TYPES = [
615
560
  {
@@ -628,16 +573,6 @@ exports.CUSTOM_TYPES = [
628
573
  },
629
574
  aspectType: CONSTRAINT_ASPECT_TYPE,
630
575
  },
631
- {
632
- id: exports.FILE_COPY_TYPE_ID,
633
- entryType: {
634
- displayName: 'Semantic Model File',
635
- description: 'A copy of one semantic model file or profile file.',
636
- },
637
- aspectType: FILE_COPY_ASPECT_TYPE,
638
- // Push stores every model file with it.
639
- required: true,
640
- },
641
576
  ];
642
577
  // Custom types are provisioned at `global` so an entry group in any region can
643
578
  // reference them, matching where the built-in system types live.
@@ -692,9 +627,8 @@ function customAspectKey(id, dest) {
692
627
  * template lacks fails with an opaque parsing error. Dataplex rejects a
693
628
  * backwards-incompatible change, so the patch only ever adds appended fields.
694
629
  *
695
- * Stops at the first hard failure, which includes a permission refusal for a
696
- * required type that does not exist. A refusal to patch a type that exists is
697
- * a warning. A refusal for an optional type does NOT stop it: the refusals
630
+ * Stops at the first hard failure. A refusal to patch a type that exists is
631
+ * a warning. A refusal to create a type does NOT stop it: the refusals
698
632
  * differ per type and per operation -- patching a type that already exists
699
633
  * needs `update`, creating one that does not needs `create` -- so a caller
700
634
  * refused one type may well be allowed the next, and giving up early would
@@ -724,62 +658,59 @@ async function provisionOne(cat, dest, type) {
724
658
  const home = customTypeHome(dest);
725
659
  // Creating a type needs permissions that publishing entries does not. A
726
660
  // caller who lacks them can still init, push and pull every model that uses
727
- // none of the optional types, so that refusal is reported rather than fatal.
728
- // A required type is needed by every push, so its refusal stops init.
661
+ // none of the custom types, so that refusal is reported rather than fatal.
729
662
  const denial = (what, status, message) => {
730
663
  if (status !== 403)
731
664
  return undefined;
732
- const refused = `no permission to create ${what} in project '${home.project}' (${message || status})`;
733
- return type.required ?
734
- { error: `${refused}; every push needs it, so init cannot finish` } :
735
- {
736
- denied: `${refused}; models that need it cannot be pushed until ` +
737
- `it exists`
738
- };
665
+ return {
666
+ denied: `no permission to create ${what} in project '${home.project}' (${message || status}); models that need it cannot be pushed ` +
667
+ `until it exists`
668
+ };
739
669
  };
740
- // A caller without create permission may be refused the create of a type
741
- // that already exists. For a required type a lookup tells the two apart, so
742
- // init fails only when the type is missing.
743
- const refusedButExists = async (status, lookup) => status === 403 && !!type.required && (await lookup()).status === 200;
744
670
  const warnings = [];
745
671
  const aspectLabel = `aspect type '${type.id}'`;
746
672
  const aspect = await cat.createAspectType(home.project, home.location, type.id, type.aspectType);
747
- if (aspect.status === 409 ||
748
- await refusedButExists(aspect.status, () => cat.getAspectType(home.project, home.location, type.id))) {
749
- const upd = await cat.updateAspectType(home.project, home.location, type.id, type.aspectType, ['description', 'display_name', 'metadata_template']);
750
- // Patching an existing type forward is best effort. Dataplex refuses a
751
- // template change it judges backwards-incompatible -- removing a field, or
752
- // renumbering one -- and the type already there is the one every published
753
- // entry depends on. Leaving it alone and saying so beats aborting an init
754
- // that has already created the entry group.
755
- //
756
- // What the warning has to carry is the way OUT, because there is no way
757
- // forward: `kcmd` creates and patches aspect types and does not delete
758
- // them, so a project holding a template this build cannot patch stays that
759
- // way until someone removes it by hand. Naming the resource and the
760
- // command is the whole of the fix; a reader who has only "left it
761
- // unchanged" has to work out which of several types in which project, and
762
- // then that `gcloud dataplex aspect-types` is where it lives.
673
+ if (aspect.status === 409) {
674
+ const upd = await cat.updateAspectType(home.project, home.location, type.id, type.aspectType, ['description', 'display_name', 'metadata_template'])
675
+ // A thrown error, such as a network failure, gets the retry warning.
676
+ .catch((err) => ({ status: 0, message: String(err?.message || err) }));
677
+ // Patching an existing type is best effort. Published entries depend on
678
+ // the type already there, so a failed patch leaves it and init goes on.
763
679
  const reason = upd.status !== 200 ?
764
680
  `${upd.message || upd.status}` :
765
- await cat.awaitOperation(upd.result, `updating ${aspectLabel}`);
681
+ await cat.awaitOperation(upd.result, 'the update operation')
682
+ .catch((err) => String(err?.message || err));
766
683
  if (upd.status === 403) {
767
684
  // The type exists, so a refusal to patch it leaves it as it is.
768
685
  warnings.push(`no permission to update the existing ${aspectLabel} in project ` +
769
686
  `'${home.project}' (${reason}); left it unchanged`);
770
687
  }
771
- else if (reason) {
688
+ else if (reason &&
689
+ /Backwards incompatible template change/i.test(reason)) {
690
+ // Dataplex refuses a backwards-incompatible template change with this
691
+ // message, and the same template gets the same answer on every run.
692
+ // kcmd never deletes a type, so the warning names the gcloud commands
693
+ // that do.
772
694
  const resource = customAspectTypeName(type.id, dest);
773
695
  warnings.push(`left the existing ${aspectLabel} unchanged (${reason}). It is ` +
774
696
  `${resource}, and Dataplex will not patch it into the template ` +
775
697
  `this build states -- a template field that was removed or ` +
776
698
  `renumbered is a backwards-incompatible change and is refused. A ` +
777
699
  `push will fail wherever the two disagree. To move it forward, ` +
778
- `delete it and re-run this command: gcloud dataplex aspect-types ` +
779
- `delete ${type.id} --project=${home.project} ` +
780
- `--location=${home.location}. Deleting takes the aspects of every ` +
781
- `entry already published with this type, so re-push those models ` +
782
- `afterwards.`);
700
+ `delete the entry type and then the aspect type, and re-run this ` +
701
+ `command: gcloud dataplex entry-types delete ${type.id} ` +
702
+ `--project=${home.project} --location=${home.location} && gcloud ` +
703
+ `dataplex aspect-types delete ${type.id} --project=${home.project} ` +
704
+ `--location=${home.location}. Deleting the entry type also deletes ` +
705
+ `every entry of that type in the project, so push the models that ` +
706
+ `had them again afterwards.`);
707
+ }
708
+ else if (reason) {
709
+ // Any other failure, such as a 429 from the write quota, a 5xx, a failed
710
+ // operation or a network error, can pass on a later run.
711
+ warnings.push(`could not update the existing ${aspectLabel} in project ` +
712
+ `'${home.project}' (${reason}). Run this command again in a minute ` +
713
+ `to retry the update.`);
783
714
  }
784
715
  }
785
716
  else if (aspect.status !== 200) {
@@ -808,10 +739,8 @@ async function provisionOne(cat, dest, type) {
808
739
  requiredAspects: [{ type: customAspectTypeName(type.id, dest) }],
809
740
  });
810
741
  const collected = warnings.length ? { warnings } : {};
811
- if (entryType.status === 409 ||
812
- await refusedButExists(entryType.status, () => cat.getEntryType(home.project, home.location, type.id))) {
742
+ if (entryType.status === 409)
813
743
  return collected;
814
- }
815
744
  const refused = denial(entryLabel, entryType.status, entryType.message);
816
745
  if (refused)
817
746
  return { ...refused, ...collected };
@@ -55,11 +55,6 @@ export declare class Namer {
55
55
  * branch on `v2Aspects`.
56
56
  */
57
57
  linkId(model: SemanticModel, rel: Relationship): string;
58
- /**
59
- * Bare entry ID for a verbatim model/profile file copy (used while
60
- * `KC_V2_ASPECTS` is off). Dotted under both layouts.
61
- */
62
- fileEntryId(modelName: string, fileName: string): string;
63
58
  }
64
59
  /** Entry-link ID for a relationship on a model. */
65
60
  export declare function relationshipLinkId(modelName: string, relName: string): string;
@@ -78,6 +73,10 @@ export declare function semanticLinkId(kind: 'link' | 'relationship' | 'extends'
78
73
  * relationship entries. Because V2 model names are restricted to letters,
79
74
  * digits, underscores and hyphens, all of which `slug()` keeps,
80
75
  * `slug(modelName)` and `modelName` are equal for valid V2 models.
76
+ *
77
+ * No push writes under `.files.` any more: an older kcmd stored a copy of
78
+ * each model and profile file there. The prefix stays so the next push
79
+ * deletes those copies.
81
80
  */
82
81
  export declare function ownedEntryIdPrefixes(modelName: string): string[];
83
82
  /**
@@ -109,13 +109,6 @@ class Namer {
109
109
  linkId(model, rel) {
110
110
  return relationshipLinkId(model.name, rel.name);
111
111
  }
112
- /**
113
- * Bare entry ID for a verbatim model/profile file copy (used while
114
- * `KC_V2_ASPECTS` is off). Dotted under both layouts.
115
- */
116
- fileEntryId(modelName, fileName) {
117
- return `${slug(modelName)}.files.${slug(fileName)}`;
118
- }
119
112
  }
120
113
  exports.Namer = Namer;
121
114
  /** Entry-link ID for a relationship on a model. */
@@ -140,6 +133,10 @@ function semanticLinkId(kind, ...parts) {
140
133
  * relationship entries. Because V2 model names are restricted to letters,
141
134
  * digits, underscores and hyphens, all of which `slug()` keeps,
142
135
  * `slug(modelName)` and `modelName` are equal for valid V2 models.
136
+ *
137
+ * No push writes under `.files.` any more: an older kcmd stored a copy of
138
+ * each model and profile file there. The prefix stays so the next push
139
+ * deletes those copies.
143
140
  */
144
141
  function ownedEntryIdPrefixes(modelName) {
145
142
  const s = slug(modelName);
@@ -1,7 +1,6 @@
1
1
  import type { Entry, EntryLink } from '../gcp/dataplex';
2
2
  import type { OverviewSidecar } from '../layouts/semantic-model';
3
3
  import { SemanticModel } from './ir';
4
- import { FileCopy } from './kc_custom_types';
5
4
  export interface KcGenerateOptions {
6
5
  project: string;
7
6
  location: string;
@@ -10,7 +9,6 @@ export interface KcGenerateOptions {
10
9
  systemTypeLocation?: string;
11
10
  emitExpressions?: boolean;
12
11
  v2Aspects?: boolean;
13
- sourceFiles?: FileCopy[];
14
12
  overviews?: ReadonlyMap<string, OverviewSidecar>;
15
13
  }
16
14
  export interface KcResources {
@@ -29,4 +27,3 @@ export interface KcResources {
29
27
  * The resources reference the built-in system types; they do not create them.
30
28
  */
31
29
  export declare function generateCatalogResources(model: SemanticModel, opts: KcGenerateOptions): KcResources;
32
- export declare function fileCopyData(file: FileCopy): Record<string, any>;
@@ -28,14 +28,6 @@
28
28
  // and `kc_actions.ts` and `kc_constraints.ts` encode the aspects they carry.
29
29
  // This module only appends the entries those two return.
30
30
  //
31
- // With the first-generation aspects (v2Aspects off), each model and profile
32
- // file the push read is also stored whole as a file copy, one
33
- // `semantic-model-file` entry per file, parented to the anchor. That type is
34
- // custom too. The aspects cannot hold the whole model, so a pull returns
35
- // these copies instead of rebuilding the file from them. The anchor of a
36
- // model with copies carries their push id in the `kcmd-push-id` label on its
37
- // entrySource.
38
- //
39
31
  // Aspect data shapes mirror the aspect types' CLOSED metadataTemplates exactly
40
32
  // (a server aspect type rejects an undeclared data field):
41
33
  // * semantic-model = { deploymentTargets: string[] }
@@ -81,11 +73,9 @@
81
73
  //
82
74
  Object.defineProperty(exports, "__esModule", { value: true });
83
75
  exports.generateCatalogResources = generateCatalogResources;
84
- exports.fileCopyData = fileCopyData;
85
76
  const deployment_target_1 = require("./deployment_target");
86
77
  const kc_actions_1 = require("./kc_actions");
87
78
  const kc_constraints_1 = require("./kc_constraints");
88
- const kc_custom_types_1 = require("./kc_custom_types");
89
79
  const kc_ids_1 = require("./kc_ids");
90
80
  const loader_1 = require("./loader");
91
81
  const osi_converter_1 = require("./osi_converter");
@@ -112,13 +102,6 @@ function generateCatalogResources(model, opts) {
112
102
  const v2 = opts.v2Aspects ?? false;
113
103
  if (v2)
114
104
  warnings.push(...unstorableMemberWarnings(model));
115
- // The first-generation aspects have no field for a vendor's extension
116
- // blocks, and the stored copy of the model file keeps them.
117
- if (!v2 && opts.sourceFiles?.length && hasThirdPartyExtensions(model)) {
118
- warnings.push('custom_extensions from vendors other than GOOGLE are not indexed ' +
119
- 'by the Knowledge Catalog aspects; the stored copy of the model file ' +
120
- 'preserves them');
121
- }
122
105
  // With the first-generation aspects, a subtype field that only rebinds an
123
106
  // inherited field is published with the definition it inherits.
124
107
  const inherited = v2 ? undefined : inheritedFields(model);
@@ -161,7 +144,9 @@ function generateCatalogResources(model, opts) {
161
144
  continue;
162
145
  entityEntryName.set(entity.name, names.entry(entityId));
163
146
  if (v2 ? lacksKeys(model, entity) : !entity.keys || !entity.keys.length) {
164
- warnings.push(`entity '${entity.name}': no keys declared in the source model`);
147
+ warnings.push(`entity '${entity.name}': no keys declared in the source model; ` +
148
+ `declare primary_key or unique_keys in the model file or in a ` +
149
+ `profile to record a key in Knowledge Catalog`);
165
150
  }
166
151
  entries.push({
167
152
  name: names.entry(entityId),
@@ -234,38 +219,6 @@ function generateCatalogResources(model, opts) {
234
219
  anchor: modelEntryName,
235
220
  claim: (id, label) => claim(seen, id, 'entry', label, warnings),
236
221
  }, warnings));
237
- // One file copy entry per model and profile file, holding its text as
238
- // read. Only with the first-generation aspects: a push with v2Aspects on
239
- // emits none, so delete reconciliation removes the copies under the owned
240
- // `.files.` prefix. Two file names that slug to one id fail the push, since
241
- // storing only one would lose the other on the next pull.
242
- if (!v2) {
243
- const fileById = new Map();
244
- let pushId;
245
- for (const file of opts.sourceFiles ?? []) {
246
- const id = names.fileEntryId(model.name, file.fileName);
247
- const earlier = fileById.get(id);
248
- if (earlier !== undefined) {
249
- throw new Error(`files '${earlier}' and '${file.fileName}' both map to entry id ` +
250
- `'${id}'; rename one of them`);
251
- }
252
- fileById.set(id, file.fileName);
253
- if (!claim(seen, id, 'entry', `file '${file.fileName}'`, warnings))
254
- continue;
255
- entries.push(fileCopyEntry(names.entry(id), modelEntryName, file, opts));
256
- pushId ??= file.pushId;
257
- }
258
- // The anchor is written before the copies and is labeled with the push id
259
- // they carry. A pull that finds the label and no copies from that push
260
- // knows the last push stopped before storing them. A model an older kcmd
261
- // pushed has no label.
262
- if (pushId !== undefined) {
263
- entries[0].entrySource = {
264
- ...entries[0].entrySource,
265
- labels: { [kc_custom_types_1.PUSH_ID_LABEL]: pushId },
266
- };
267
- }
268
- }
269
222
  const entryLinks = v2Relationships ?
270
223
  [
271
224
  ...v2Relationships.links,
@@ -497,42 +450,6 @@ function schemaJoinAspectData(model, rel) {
497
450
  function entityByName(model, name) {
498
451
  return (model.entities ?? []).find(e => e.name === name);
499
452
  }
500
- // The entry holding one file copy. Its entry and aspect types are custom, so
501
- // they live in the destination project.
502
- function fileCopyEntry(name, anchor, file, dest) {
503
- return {
504
- name,
505
- entryType: (0, kc_custom_types_1.customEntryTypeName)(kc_custom_types_1.FILE_COPY_TYPE_ID, dest),
506
- parentEntry: anchor,
507
- aspects: {
508
- [(0, kc_custom_types_1.customAspectKey)(kc_custom_types_1.FILE_COPY_TYPE_ID, dest)]: {
509
- aspectType: (0, kc_custom_types_1.customAspectTypeName)(kc_custom_types_1.FILE_COPY_TYPE_ID, dest),
510
- data: fileCopyData(file),
511
- },
512
- },
513
- };
514
- }
515
- // The aspect data of a file copy: the file's name and text, and the push id
516
- // and copy count when the push set them.
517
- function fileCopyData(file) {
518
- return compact({
519
- fileName: file.fileName,
520
- content: file.content,
521
- pushId: file.pushId,
522
- pushFileCount: file.pushFileCount,
523
- });
524
- }
525
- // True when the model, or anything in it, carries a `custom_extensions` block
526
- // from a vendor other than GOOGLE.
527
- function hasThirdPartyExtensions(model) {
528
- const thirdParty = (o) => (o.customExtensions ?? []).some(x => x.vendorName !== 'GOOGLE');
529
- const entities = model.entities ?? [];
530
- return thirdParty(model) || entities.some(thirdParty) ||
531
- entities.some(e => (e.fields ?? []).some(thirdParty)) ||
532
- (model.relationships ?? []).some(thirdParty) ||
533
- (model.metrics ?? []).some(thirdParty) ||
534
- (model.actions ?? []).some(thirdParty);
535
- }
536
453
  // One warning for each custom `ai_context` member that `guidelines` leaves
537
454
  // out, on the model, an entity, a field, a metric or a relationship.
538
455
  function unstorableMemberWarnings(model) {