@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.
- package/README.md +18 -20
- package/build/ts/kcmd/cli.js +6164 -5631
- package/build/ts/kcmd/gcp/biglake.d.ts +2 -0
- package/build/ts/kcmd/gcp/biglake.js +13 -4
- package/build/ts/kcmd/gcp/spanner.d.ts +10 -0
- package/build/ts/kcmd/gcp/spanner.js +6 -3
- package/build/ts/kcmd/layouts/okf.js +49 -7
- package/build/ts/kcmd/layouts/semantic-model.d.ts +13 -0
- package/build/ts/kcmd/layouts/semantic-model.js +129 -50
- package/build/ts/kcmd/okf/concept.d.ts +1 -1
- package/build/ts/kcmd/okf/concept.js +6 -0
- package/build/ts/kcmd/okf/concept_to_entry.d.ts +2 -0
- package/build/ts/kcmd/okf/concept_to_entry.js +29 -2
- package/build/ts/kcmd/okf/init_okf_workspace.js +7 -9
- package/build/ts/kcmd/okf/okf_metadata.js +17 -0
- package/build/ts/kcmd/semantic/bigquery.js +139 -195
- package/build/ts/kcmd/semantic/binding.js +14 -19
- package/build/ts/kcmd/semantic/deploy_bigquery.js +6 -13
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +10 -35
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +105 -162
- package/build/ts/kcmd/semantic/deploy_spanner.js +7 -14
- package/build/ts/kcmd/semantic/deployment_target.d.ts +1 -4
- package/build/ts/kcmd/semantic/deployment_target.js +6 -62
- package/build/ts/kcmd/semantic/graph_ddl.d.ts +10 -0
- package/build/ts/kcmd/semantic/graph_ddl.js +152 -59
- package/build/ts/kcmd/semantic/import_model.d.ts +26 -4
- package/build/ts/kcmd/semantic/import_model.js +531 -111
- package/build/ts/kcmd/semantic/ir.d.ts +2 -4
- package/build/ts/kcmd/semantic/kc_converter.d.ts +7 -1
- package/build/ts/kcmd/semantic/kc_converter.js +32 -27
- package/build/ts/kcmd/semantic/kc_custom_types.d.ts +5 -12
- package/build/ts/kcmd/semantic/kc_custom_types.js +43 -114
- package/build/ts/kcmd/semantic/kc_ids.d.ts +4 -5
- package/build/ts/kcmd/semantic/kc_ids.js +4 -7
- package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +0 -3
- package/build/ts/kcmd/semantic/knowledge_catalog.js +3 -86
- package/build/ts/kcmd/semantic/loader.d.ts +7 -11
- package/build/ts/kcmd/semantic/loader.js +282 -171
- package/build/ts/kcmd/semantic/osi_converter.js +14 -65
- package/build/ts/kcmd/semantic/parameters.d.ts +1 -0
- package/build/ts/kcmd/semantic/parameters.js +22 -3
- package/build/ts/kcmd/semantic/pull_kc.d.ts +1 -5
- package/build/ts/kcmd/semantic/pull_kc.js +38 -112
- package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +7 -2
- package/build/ts/kcmd/semantic/resolve_inheritance.js +37 -21
- package/build/ts/kcmd/semantic/resolve_profiles.d.ts +51 -10
- package/build/ts/kcmd/semantic/resolve_profiles.js +213 -32
- package/build/ts/kcmd/semantic/runtime/runtime.d.ts +4 -12
- package/build/ts/kcmd/semantic/runtime/runtime.js +4 -21
- package/build/ts/kcmd/semantic/runtime/store.d.ts +14 -19
- package/build/ts/kcmd/semantic/runtime/store.js +21 -37
- package/build/ts/kcmd/semantic/source_dialect.d.ts +9 -2
- package/build/ts/kcmd/semantic/source_dialect.js +83 -33
- package/build/ts/kcmd/semantic/source_entries.d.ts +1 -1
- package/build/ts/kcmd/semantic/source_entries.js +95 -36
- package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +13 -7
- package/build/ts/kcmd/semantic/sql_expr_utils.js +240 -67
- package/build/ts/kcmd/semantic/sql_parser.d.ts +5 -2
- package/build/ts/kcmd/semantic/sql_parser.js +22 -0
- package/build/ts/kcmd/semantic/transpile.d.ts +12 -10
- package/build/ts/kcmd/semantic/transpile.js +79 -61
- package/build/ts/kcmd/semantic/validate.d.ts +37 -2
- package/build/ts/kcmd/semantic/validate.js +467 -126
- package/build/ts/kcmd/snapshot.d.ts +4 -0
- package/build/ts/kcmd/snapshot.js +7 -0
- package/build/ts/kcmd/sync.js +44 -7
- 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
|
|
37
|
-
*
|
|
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
|
|
35
|
-
// `
|
|
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
|
|
132
|
-
|
|
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
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
//
|
|
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
|
|
15
|
-
export declare const
|
|
16
|
-
export
|
|
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
|
|
60
|
-
*
|
|
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.
|
|
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
|
|
553
|
-
//
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
|
696
|
-
*
|
|
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
|
|
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
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
//
|
|
752
|
-
//
|
|
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,
|
|
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
|
|
779
|
-
`delete ${type.id}
|
|
780
|
-
`--location=${home.location}
|
|
781
|
-
`
|
|
782
|
-
`
|
|
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) {
|