@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.
- package/LICENSE +203 -0
- package/README.md +264 -2
- package/build/ts/kcmd/cli.js +20252 -0
- package/build/ts/kcmd/gcp/alloydb.d.ts +12 -0
- package/build/ts/kcmd/gcp/alloydb.js +70 -0
- package/build/ts/kcmd/gcp/api.d.ts +29 -0
- package/build/ts/kcmd/gcp/api.js +162 -0
- package/build/ts/kcmd/gcp/biglake.d.ts +18 -0
- package/build/ts/kcmd/gcp/biglake.js +59 -0
- package/build/ts/kcmd/gcp/bigquery.d.ts +57 -0
- package/build/ts/kcmd/gcp/bigquery.js +134 -0
- package/build/ts/kcmd/gcp/context.d.ts +10 -0
- package/build/ts/kcmd/gcp/context.js +74 -0
- package/build/ts/kcmd/gcp/crm.d.ts +13 -0
- package/build/ts/kcmd/gcp/crm.js +91 -0
- package/build/ts/kcmd/gcp/dataplex.d.ts +147 -0
- package/build/ts/kcmd/gcp/dataplex.js +406 -0
- package/build/ts/kcmd/gcp/index.d.ts +3 -0
- package/build/ts/kcmd/gcp/index.js +21 -0
- package/build/ts/kcmd/gcp/spanner.d.ts +73 -0
- package/build/ts/kcmd/gcp/spanner.js +126 -0
- package/build/ts/kcmd/index.d.ts +7 -0
- package/build/ts/kcmd/index.js +46 -0
- package/build/ts/kcmd/layout.d.ts +16 -0
- package/build/ts/kcmd/layout.js +31 -0
- package/build/ts/kcmd/layouts/documents.d.ts +24 -0
- package/build/ts/kcmd/layouts/documents.js +264 -0
- package/build/ts/kcmd/layouts/okf.d.ts +31 -0
- package/build/ts/kcmd/layouts/okf.js +386 -0
- package/build/ts/kcmd/layouts/semantic-model.d.ts +74 -0
- package/build/ts/kcmd/layouts/semantic-model.js +621 -0
- package/build/ts/kcmd/layouts/standard.d.ts +13 -0
- package/build/ts/kcmd/layouts/standard.js +108 -0
- package/build/ts/kcmd/manifest.d.ts +34 -0
- package/build/ts/kcmd/manifest.js +262 -0
- package/build/ts/kcmd/metadata.d.ts +25 -0
- package/build/ts/kcmd/metadata.js +8 -0
- package/build/ts/kcmd/okf/catalog_residue.d.ts +14 -0
- package/build/ts/kcmd/okf/catalog_residue.js +124 -0
- package/build/ts/kcmd/okf/concept.d.ts +14 -0
- package/build/ts/kcmd/okf/concept.js +110 -0
- package/build/ts/kcmd/okf/concept_to_entry.d.ts +10 -0
- package/build/ts/kcmd/okf/concept_to_entry.js +281 -0
- package/build/ts/kcmd/okf/init_okf_workspace.d.ts +15 -0
- package/build/ts/kcmd/okf/init_okf_workspace.js +113 -0
- package/build/ts/kcmd/okf/links.d.ts +23 -0
- package/build/ts/kcmd/okf/links.js +468 -0
- package/build/ts/kcmd/okf/okf_metadata.d.ts +12 -0
- package/build/ts/kcmd/okf/okf_metadata.js +99 -0
- package/build/ts/kcmd/semantic/bigquery.d.ts +22 -0
- package/build/ts/kcmd/semantic/bigquery.js +469 -0
- package/build/ts/kcmd/semantic/binding.d.ts +3 -0
- package/build/ts/kcmd/semantic/binding.js +92 -0
- package/build/ts/kcmd/semantic/catalog_entry.d.ts +24 -0
- package/build/ts/kcmd/semantic/catalog_entry.js +35 -0
- package/build/ts/kcmd/semantic/converters/owl/convert.d.ts +26 -0
- package/build/ts/kcmd/semantic/converters/owl/convert.js +41 -0
- package/build/ts/kcmd/semantic/converters/owl/model.d.ts +98 -0
- package/build/ts/kcmd/semantic/converters/owl/model.js +30 -0
- package/build/ts/kcmd/semantic/converters/owl/parse.d.ts +11 -0
- package/build/ts/kcmd/semantic/converters/owl/parse.js +634 -0
- package/build/ts/kcmd/semantic/converters/owl/to_ir.d.ts +19 -0
- package/build/ts/kcmd/semantic/converters/owl/to_ir.js +390 -0
- package/build/ts/kcmd/semantic/deploy_bigquery.d.ts +20 -0
- package/build/ts/kcmd/semantic/deploy_bigquery.js +226 -0
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +104 -0
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +1367 -0
- package/build/ts/kcmd/semantic/deploy_spanner.d.ts +15 -0
- package/build/ts/kcmd/semantic/deploy_spanner.js +150 -0
- package/build/ts/kcmd/semantic/deployment_target.d.ts +93 -0
- package/build/ts/kcmd/semantic/deployment_target.js +379 -0
- package/build/ts/kcmd/semantic/graph_ddl.d.ts +44 -0
- package/build/ts/kcmd/semantic/graph_ddl.js +656 -0
- package/build/ts/kcmd/semantic/import_model.d.ts +72 -0
- package/build/ts/kcmd/semantic/import_model.js +1698 -0
- package/build/ts/kcmd/semantic/ir.d.ts +589 -0
- package/build/ts/kcmd/semantic/ir.js +247 -0
- package/build/ts/kcmd/semantic/kc_actions.d.ts +45 -0
- package/build/ts/kcmd/semantic/kc_actions.js +531 -0
- package/build/ts/kcmd/semantic/kc_constraints.d.ts +44 -0
- package/build/ts/kcmd/semantic/kc_constraints.js +279 -0
- package/build/ts/kcmd/semantic/kc_converter.d.ts +37 -0
- package/build/ts/kcmd/semantic/kc_converter.js +839 -0
- package/build/ts/kcmd/semantic/kc_custom_types.d.ts +63 -0
- package/build/ts/kcmd/semantic/kc_custom_types.js +751 -0
- package/build/ts/kcmd/semantic/kc_entries.d.ts +155 -0
- package/build/ts/kcmd/semantic/kc_entries.js +284 -0
- package/build/ts/kcmd/semantic/kc_ids.d.ts +102 -0
- package/build/ts/kcmd/semantic/kc_ids.js +188 -0
- package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +29 -0
- package/build/ts/kcmd/semantic/knowledge_catalog.js +960 -0
- package/build/ts/kcmd/semantic/loader.d.ts +258 -0
- package/build/ts/kcmd/semantic/loader.js +3576 -0
- package/build/ts/kcmd/semantic/names.d.ts +10 -0
- package/build/ts/kcmd/semantic/names.js +85 -0
- package/build/ts/kcmd/semantic/osi_converter.d.ts +60 -0
- package/build/ts/kcmd/semantic/osi_converter.js +910 -0
- package/build/ts/kcmd/semantic/parameters.d.ts +19 -0
- package/build/ts/kcmd/semantic/parameters.js +171 -0
- package/build/ts/kcmd/semantic/pull_kc.d.ts +10 -0
- package/build/ts/kcmd/semantic/pull_kc.js +272 -0
- package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +59 -0
- package/build/ts/kcmd/semantic/resolve_inheritance.js +346 -0
- package/build/ts/kcmd/semantic/resolve_profiles.d.ts +291 -0
- package/build/ts/kcmd/semantic/resolve_profiles.js +2003 -0
- package/build/ts/kcmd/semantic/runtime/agent_tools.d.ts +93 -0
- package/build/ts/kcmd/semantic/runtime/agent_tools.js +243 -0
- package/build/ts/kcmd/semantic/runtime/dialect.d.ts +13 -0
- package/build/ts/kcmd/semantic/runtime/dialect.js +39 -0
- package/build/ts/kcmd/semantic/runtime/runtime.d.ts +88 -0
- package/build/ts/kcmd/semantic/runtime/runtime.js +301 -0
- package/build/ts/kcmd/semantic/runtime/store.d.ts +112 -0
- package/build/ts/kcmd/semantic/runtime/store.js +344 -0
- package/build/ts/kcmd/semantic/skills.d.ts +110 -0
- package/build/ts/kcmd/semantic/skills.js +697 -0
- package/build/ts/kcmd/semantic/source_dialect.d.ts +17 -0
- package/build/ts/kcmd/semantic/source_dialect.js +311 -0
- package/build/ts/kcmd/semantic/source_entries.d.ts +12 -0
- package/build/ts/kcmd/semantic/source_entries.js +341 -0
- package/build/ts/kcmd/semantic/spanner.d.ts +18 -0
- package/build/ts/kcmd/semantic/spanner.js +60 -0
- package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +23 -0
- package/build/ts/kcmd/semantic/sql_expr_utils.js +394 -0
- package/build/ts/kcmd/semantic/sql_identifiers.d.ts +6 -0
- package/build/ts/kcmd/semantic/sql_identifiers.js +267 -0
- package/build/ts/kcmd/semantic/sql_parser.d.ts +47 -0
- package/build/ts/kcmd/semantic/sql_parser.js +400 -0
- package/build/ts/kcmd/semantic/transpile.d.ts +76 -0
- package/build/ts/kcmd/semantic/transpile.js +581 -0
- package/build/ts/kcmd/semantic/validate.d.ts +70 -0
- package/build/ts/kcmd/semantic/validate.js +2266 -0
- package/build/ts/kcmd/semantic/yaml_options.d.ts +6 -0
- package/build/ts/kcmd/semantic/yaml_options.js +58 -0
- package/build/ts/kcmd/semantic/yaml_update.d.ts +26 -0
- package/build/ts/kcmd/semantic/yaml_update.js +782 -0
- package/build/ts/kcmd/snapshot.d.ts +37 -0
- package/build/ts/kcmd/snapshot.js +386 -0
- package/build/ts/kcmd/source.d.ts +31 -0
- package/build/ts/kcmd/source.js +119 -0
- package/build/ts/kcmd/sources/bq-dataset.d.ts +17 -0
- package/build/ts/kcmd/sources/bq-dataset.js +131 -0
- package/build/ts/kcmd/sources/entrygroup.d.ts +21 -0
- package/build/ts/kcmd/sources/entrygroup.js +83 -0
- package/build/ts/kcmd/sources/kb.d.ts +21 -0
- package/build/ts/kcmd/sources/kb.js +82 -0
- package/build/ts/kcmd/sources/semantic-model.d.ts +18 -0
- package/build/ts/kcmd/sources/semantic-model.js +48 -0
- package/build/ts/kcmd/sync.d.ts +35 -0
- package/build/ts/kcmd/sync.js +734 -0
- package/package.json +62 -4
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// How a model's ACTIONS are encoded in Knowledge Catalog.
|
|
3
|
+
//
|
|
4
|
+
// An action is published exactly the way a metric is: one entry per action,
|
|
5
|
+
// parented to the model anchor, carrying one aspect that holds the executor and
|
|
6
|
+
// the typed parameters. What differs is the type. Every other construct has a
|
|
7
|
+
// built-in system type, and an action does not, so it uses the custom
|
|
8
|
+
// `semantic-action` pair DECLARED IN kc_custom_types.ts and created there by
|
|
9
|
+
// `kcmd init --semantic-model`. That file is the list of what is custom; this
|
|
10
|
+
// one is only the encoding that fills the aspect.
|
|
11
|
+
//
|
|
12
|
+
// The custom pair is what makes an action explicit in the catalog. A search can
|
|
13
|
+
// tell an action apart from anything else by its entry type, and the aspect's
|
|
14
|
+
// fields are typed and queryable rather than prose a reader has to interpret.
|
|
15
|
+
//
|
|
16
|
+
// WHEN A BUILT-IN ACTION TYPE SHIPS, follow the instructions at the top of
|
|
17
|
+
// kc_custom_types.ts. Nothing in this file changes: the readers below match a
|
|
18
|
+
// type by its id suffix, so they do not care which project it lives in.
|
|
19
|
+
//
|
|
20
|
+
// The call sites are `knowledge_catalog.ts` (emit the entries),
|
|
21
|
+
// `kc_converter.ts` (read them back), and `pull_kc.ts` (hydrate the aspect).
|
|
22
|
+
//
|
|
23
|
+
// `instructions` rides the action's own aspect rather than the built-in
|
|
24
|
+
// `guidelines` aspect that an entity or metric uses. A pull derives which
|
|
25
|
+
// aspect types to hydrate from the project the ENTRY type lives in, so an
|
|
26
|
+
// action entry, whose type is custom and therefore in the destination project,
|
|
27
|
+
// would ask for a `guidelines` aspect type that only exists under
|
|
28
|
+
// `dataplex-types`. Keeping the field on the action's own aspect keeps the
|
|
29
|
+
// whole encoding inside the one type kc_custom_types.ts provisions.
|
|
30
|
+
//
|
|
31
|
+
// The helpers at the bottom duplicate a few lines from the modules above on
|
|
32
|
+
// purpose. This module imports only the IR, the entry shape and the type
|
|
33
|
+
// registry, so it can be swapped or deleted as a unit.
|
|
34
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
exports.actionEntryTypeName = actionEntryTypeName;
|
|
36
|
+
exports.actionAspectTypeName = actionAspectTypeName;
|
|
37
|
+
exports.actionAspectKey = actionAspectKey;
|
|
38
|
+
exports.actionEntryId = actionEntryId;
|
|
39
|
+
exports.actionOwnedPrefix = actionOwnedPrefix;
|
|
40
|
+
exports.actionEntries = actionEntries;
|
|
41
|
+
exports.isActionEntry = isActionEntry;
|
|
42
|
+
exports.actionAspectTypes = actionAspectTypes;
|
|
43
|
+
exports.readAction = readAction;
|
|
44
|
+
const ir_1 = require("./ir");
|
|
45
|
+
const kc_custom_types_1 = require("./kc_custom_types");
|
|
46
|
+
const resolve_inheritance_1 = require("./resolve_inheritance");
|
|
47
|
+
// Full resource name of the action entry type for a destination.
|
|
48
|
+
function actionEntryTypeName(dest) {
|
|
49
|
+
return (0, kc_custom_types_1.customEntryTypeName)(kc_custom_types_1.ACTION_TYPE_ID, dest);
|
|
50
|
+
}
|
|
51
|
+
// Full resource name of the action aspect type for a destination.
|
|
52
|
+
function actionAspectTypeName(dest) {
|
|
53
|
+
return (0, kc_custom_types_1.customAspectTypeName)(kc_custom_types_1.ACTION_TYPE_ID, dest);
|
|
54
|
+
}
|
|
55
|
+
// Aspect-map key: the `project.location.type` reference form the client keys an
|
|
56
|
+
// entry's aspects by.
|
|
57
|
+
function actionAspectKey(dest) {
|
|
58
|
+
return (0, kc_custom_types_1.customAspectKey)(kc_custom_types_1.ACTION_TYPE_ID, dest);
|
|
59
|
+
}
|
|
60
|
+
// The entry id of one action: `<model>.actions.<name>`, alongside
|
|
61
|
+
// `<model>.entities.<name>` and `<model>.metrics.<name>`.
|
|
62
|
+
function actionEntryId(modelId, actionName) {
|
|
63
|
+
return `${modelId}.actions.${slug(actionName)}`;
|
|
64
|
+
}
|
|
65
|
+
// The entry-id prefix a model's actions occupy, so delete reconciliation
|
|
66
|
+
// removes the entry of an action dropped from the model.
|
|
67
|
+
function actionOwnedPrefix(modelId) {
|
|
68
|
+
return `${modelId}.actions.`;
|
|
69
|
+
}
|
|
70
|
+
// Why a pull will not recover the field a parameter projects from, or '' when
|
|
71
|
+
// it will. Three ways to lose it, and naming which one is the difference
|
|
72
|
+
// between an author deleting the projection and an author binding a column:
|
|
73
|
+
//
|
|
74
|
+
// - the concept is not published at all (an abstract entity under the
|
|
75
|
+
// first-generation types);
|
|
76
|
+
// - the concept is published but the FIELD is not -- `pruneUnavailable`
|
|
77
|
+
// drops an unbound field while keeping the entity whose key still binds,
|
|
78
|
+
// and a projected parameter no longer prunes its action, so the entry goes
|
|
79
|
+
// out naming a field the `schema` aspect does not carry;
|
|
80
|
+
// - the concept is a relationship. Pull rebuilds relationships from
|
|
81
|
+
// `schema-join` entry links alone, and a link records its two endpoints
|
|
82
|
+
// and nothing else, so an association's own fields come back from no
|
|
83
|
+
// entry. The parameter still publishes intact either way -- the loader
|
|
84
|
+
// resolved its type and wording before any of this -- so what is lost is
|
|
85
|
+
// only the projection, and only on the way back.
|
|
86
|
+
function unrecoverable(concept, field, relationships, published, ctx) {
|
|
87
|
+
if (!ctx.publishedEntities.has(concept) && relationships.has(concept)) {
|
|
88
|
+
return `'${concept}' is a relationship, whose fields no entry records`;
|
|
89
|
+
}
|
|
90
|
+
if (!ctx.publishedEntities.has(concept)) {
|
|
91
|
+
return `this push does not publish '${concept}' (abstract or unavailable)`;
|
|
92
|
+
}
|
|
93
|
+
if (published && field !== undefined &&
|
|
94
|
+
!published.get(concept)?.fields.has(field)) {
|
|
95
|
+
return `this push publishes '${concept}' without its '${field}' field ` +
|
|
96
|
+
`(unbound under this profile)`;
|
|
97
|
+
}
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* One entry per action, to append to the model's entries.
|
|
102
|
+
*
|
|
103
|
+
* Empty when the model declares no actions, so a model without actions is
|
|
104
|
+
* unchanged from before actions existed. Warns once when it is non-empty:
|
|
105
|
+
* actions reach Knowledge Catalog and nowhere else, which is worth saying out
|
|
106
|
+
* loud on a push that also deploys a graph.
|
|
107
|
+
*/
|
|
108
|
+
function actionEntries(model, modelId, ctx, warnings) {
|
|
109
|
+
const actions = model.actions ?? [];
|
|
110
|
+
if (!actions.length)
|
|
111
|
+
return [];
|
|
112
|
+
// A relationship is a perfectly good `affects` concept but is never in
|
|
113
|
+
// publishedEntities, so the check below reads the model's own edges for it.
|
|
114
|
+
// Pruning drops a whole relationship from the model, so a dropped one is
|
|
115
|
+
// absent from this set for the same reason a dropped entity is absent from
|
|
116
|
+
// publishedEntities.
|
|
117
|
+
const relationshipNames = new Set((model.relationships ?? []).map(r => r.name));
|
|
118
|
+
// The fields of each concept with inheritance resolved, which a later pull
|
|
119
|
+
// rebuilds from the catalog. Asked of the loader's own resolver so the
|
|
120
|
+
// answer accounts for `extends`; a model that will not resolve returns null
|
|
121
|
+
// and the field half of the check is skipped, since an emitter is not the
|
|
122
|
+
// place to discover that a model does not load.
|
|
123
|
+
let published;
|
|
124
|
+
try {
|
|
125
|
+
published = (0, resolve_inheritance_1.declaredConceptFields)(model);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
published = null;
|
|
129
|
+
}
|
|
130
|
+
const entries = [];
|
|
131
|
+
for (const action of actions) {
|
|
132
|
+
const id = actionEntryId(modelId, action.name);
|
|
133
|
+
if (!ctx.claim(id, `action '${action.name}'`))
|
|
134
|
+
continue;
|
|
135
|
+
// A derived parameter -- one projected from a field -- leaves the catalog
|
|
136
|
+
// naming a concept it has no entry for when that concept is not part of
|
|
137
|
+
// this push. The parameter itself still publishes intact, type and wording
|
|
138
|
+
// and all, because the loader already resolved them; what a later pull
|
|
139
|
+
// cannot do is find the field the projection came from, so re-authoring
|
|
140
|
+
// that model would have to state the type by hand.
|
|
141
|
+
for (const p of action.parameters ?? []) {
|
|
142
|
+
if (!p.concept)
|
|
143
|
+
continue;
|
|
144
|
+
const why = unrecoverable(p.concept, p.field, relationshipNames, published, ctx);
|
|
145
|
+
if (!why)
|
|
146
|
+
continue;
|
|
147
|
+
warnings.push(`model '${model.name}': action '${action.name}' parameter ` +
|
|
148
|
+
`'${p.name}' is projected from '${p.concept}.${p.field}', ` +
|
|
149
|
+
`${why}, so a pull will recover the parameter but not the field ` +
|
|
150
|
+
`it came from.`);
|
|
151
|
+
}
|
|
152
|
+
// The same hazard for the concepts the action declares it changes. The
|
|
153
|
+
// entry is still published -- the action does change that concept, and
|
|
154
|
+
// dropping the claim would understate the blast radius -- but the catalog
|
|
155
|
+
// then names a concept it has no entry for, and pulling that model and
|
|
156
|
+
// pushing it unpruned fails hard on the same name. Say it where the
|
|
157
|
+
// divergence is created rather than two commands later.
|
|
158
|
+
for (const affected of action.affects ?? []) {
|
|
159
|
+
if (ctx.publishedEntities.has(affected.concept))
|
|
160
|
+
continue;
|
|
161
|
+
if (relationshipNames.has(affected.concept))
|
|
162
|
+
continue;
|
|
163
|
+
warnings.push(`model '${model.name}': action '${action.name}' affects ` +
|
|
164
|
+
`'${affected.concept}', which this push does not publish ` +
|
|
165
|
+
`(abstract or unavailable), so the catalog records a blast radius ` +
|
|
166
|
+
`naming a concept it has no entry for.`);
|
|
167
|
+
}
|
|
168
|
+
entries.push({
|
|
169
|
+
name: ctx.entry(id),
|
|
170
|
+
entryType: actionEntryTypeName(ctx),
|
|
171
|
+
parentEntry: ctx.anchor,
|
|
172
|
+
entrySource: compact({
|
|
173
|
+
displayName: action.name,
|
|
174
|
+
description: action.description,
|
|
175
|
+
}),
|
|
176
|
+
aspects: {
|
|
177
|
+
[actionAspectKey(ctx)]: {
|
|
178
|
+
aspectType: actionAspectTypeName(ctx),
|
|
179
|
+
data: actionAspectData(action),
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
// Counted from what was emitted rather than from the model, so a name
|
|
185
|
+
// collision that skipped an action does not inflate the number.
|
|
186
|
+
if (entries.length)
|
|
187
|
+
warnings.push(`model '${model.name}': ${entries.length} action(s) published as ` +
|
|
188
|
+
`${kc_custom_types_1.ACTION_TYPE_ID} entries (Knowledge Catalog is the only system an ` +
|
|
189
|
+
`action reaches).`);
|
|
190
|
+
return entries;
|
|
191
|
+
}
|
|
192
|
+
// The aspect payload for one action: the executor flattened to the fields of
|
|
193
|
+
// its kind, the typed parameters, the constraints that gate it, what it
|
|
194
|
+
// changes, and any AI instructions. Guards are the constraint NAMES, matching
|
|
195
|
+
// the sibling `semantic-constraint` entries by display name, so a reader
|
|
196
|
+
// holding one action entry can find the rules it is checked against; an
|
|
197
|
+
// an affected concept names an entity or relationship entry the same way.
|
|
198
|
+
function actionAspectData(action) {
|
|
199
|
+
return compact({
|
|
200
|
+
...(action.executor ? executorData(action.executor) : {}),
|
|
201
|
+
parameters: action.parameters.map(p => compact({
|
|
202
|
+
name: p.name,
|
|
203
|
+
type: p.type,
|
|
204
|
+
concept: p.concept,
|
|
205
|
+
field: p.field,
|
|
206
|
+
description: p.description,
|
|
207
|
+
required: p.required,
|
|
208
|
+
default: p.default !== undefined ? JSON.stringify(p.default) :
|
|
209
|
+
undefined,
|
|
210
|
+
})),
|
|
211
|
+
guards: action.guards?.length ? action.guards : undefined,
|
|
212
|
+
affects: action.affects?.length ? action.affects.map(affectedConceptData) :
|
|
213
|
+
undefined,
|
|
214
|
+
instructions: action.aiContext?.instructions || undefined,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
// One affected concept as its aspect record: exactly the three fields the IR
|
|
218
|
+
// carries, so
|
|
219
|
+
// the round-trip is a rename away from an identity.
|
|
220
|
+
function affectedConceptData(affected) {
|
|
221
|
+
return compact({
|
|
222
|
+
concept: affected.concept,
|
|
223
|
+
operation: affected.operation,
|
|
224
|
+
fields: affected.fields?.length ? [...affected.fields] : undefined,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
// The executor's kind plus the two coordinates that kind uses. Only the fields
|
|
228
|
+
// of the live kind are set, so the aspect never shows blanks for the others.
|
|
229
|
+
function executorData(ex) {
|
|
230
|
+
switch (ex.kind) {
|
|
231
|
+
case 'mcp':
|
|
232
|
+
return {
|
|
233
|
+
executorKind: 'mcp',
|
|
234
|
+
mcpServer: ex.mcp.server,
|
|
235
|
+
mcpTool: ex.mcp.tool,
|
|
236
|
+
};
|
|
237
|
+
case 'rest':
|
|
238
|
+
return {
|
|
239
|
+
executorKind: 'rest',
|
|
240
|
+
restEndpoint: ex.rest.endpoint,
|
|
241
|
+
restMethod: ex.rest.method,
|
|
242
|
+
};
|
|
243
|
+
case 'grpc':
|
|
244
|
+
return {
|
|
245
|
+
executorKind: 'grpc',
|
|
246
|
+
grpcService: ex.grpc.service,
|
|
247
|
+
grpcMethod: ex.grpc.method,
|
|
248
|
+
};
|
|
249
|
+
case 'sql':
|
|
250
|
+
// The statements are published verbatim. A consumer that only routes on
|
|
251
|
+
// the kind can ignore them; one that wants to know what the action
|
|
252
|
+
// actually writes -- a review engine deciding whether the declared
|
|
253
|
+
// `affects` matches the write -- has the text without a second lookup.
|
|
254
|
+
return {
|
|
255
|
+
executorKind: 'sql',
|
|
256
|
+
sqlStatements: [...ex.sql.statements],
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// Read side: an action entry -> the IR.
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// True when an entry is one of a model's actions, matched by the entry type's
|
|
264
|
+
// id suffix so the project the type lives in need not be known -- which is what
|
|
265
|
+
// lets a pull keep working when the custom type is replaced by a built-in one.
|
|
266
|
+
function isActionEntry(entry) {
|
|
267
|
+
return entry.entryType?.endsWith(`/entryTypes/${kc_custom_types_1.ACTION_TYPE_ID}`) ?? false;
|
|
268
|
+
}
|
|
269
|
+
// The aspect type resource names to hydrate for an action entry. Named through
|
|
270
|
+
// the entry type's own project so the pull follows the type wherever it lives.
|
|
271
|
+
function actionAspectTypes(entryTypeBase) {
|
|
272
|
+
return [`${entryTypeBase}/aspectTypes/${kc_custom_types_1.ACTION_TYPE_ID}`];
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Recovers one action from its entry, the inverse of actionEntries.
|
|
276
|
+
*
|
|
277
|
+
* A parameter recovers exactly what was stored, projection and all. Nothing is
|
|
278
|
+
* re-derived here: a derived parameter's type was resolved once, at load, and
|
|
279
|
+
* the aspect carries the answer, so a pull that recovered the concept and one
|
|
280
|
+
* that did not produce the same parameter rather than two different ones.
|
|
281
|
+
*
|
|
282
|
+
* An entry with NO executor recovers as an action with none: that is a legal
|
|
283
|
+
* published state, not damage. An entry whose executor names a kind but lacks
|
|
284
|
+
* the coordinates that kind needs is malformed, and returns undefined with a
|
|
285
|
+
* warning, so one bad entry degrades itself rather than the pull.
|
|
286
|
+
*/
|
|
287
|
+
function readAction(entry, warnings) {
|
|
288
|
+
const name = entry.entrySource?.displayName || idOf(entry.name);
|
|
289
|
+
const data = actionAspectDataOf(entry);
|
|
290
|
+
// No kind at all is an action no binding performed, which round-trips as it
|
|
291
|
+
// was published. A kind whose coordinates are missing is damage.
|
|
292
|
+
const declaresExecutor = typeof data.executorKind === 'string' && data.executorKind.trim() !== '';
|
|
293
|
+
const executor = readExecutor(data);
|
|
294
|
+
if (declaresExecutor && !executor) {
|
|
295
|
+
warnings.push(`action '${name}': the ${kc_custom_types_1.ACTION_TYPE_ID} aspect has no usable ` +
|
|
296
|
+
`executor; the action is skipped`);
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
const parameters = asArray(data.parameters)
|
|
300
|
+
.map((p) => readParameter(p, name, warnings))
|
|
301
|
+
.filter((p) => p !== undefined);
|
|
302
|
+
const action = { name, parameters };
|
|
303
|
+
if (executor)
|
|
304
|
+
action.executor = executor;
|
|
305
|
+
// Guard names round-trip verbatim. A name whose constraint is not part of
|
|
306
|
+
// this pull is kept rather than dropped, because dropping it here would
|
|
307
|
+
// silently rewrite the author's model. It is not kept in silence:
|
|
308
|
+
// kc_converter warns once it has both lists in hand, and push-side validate
|
|
309
|
+
// rejects it.
|
|
310
|
+
//
|
|
311
|
+
// A REPEATED name is the exception, because the loader rejects one outright:
|
|
312
|
+
// keeping it would hand back a document that cannot be reloaded. A duplicate
|
|
313
|
+
// guard checks the same constraint twice, so dropping it loses no meaning.
|
|
314
|
+
const guards = [];
|
|
315
|
+
for (const g of asArray(data.guards)) {
|
|
316
|
+
if (typeof g !== 'string' || g === '')
|
|
317
|
+
continue;
|
|
318
|
+
if (guards.includes(g)) {
|
|
319
|
+
warnings.push(`action '${name}': the ${kc_custom_types_1.ACTION_TYPE_ID} aspect repeats guard ` +
|
|
320
|
+
`'${g}'; the duplicate is dropped so the document still loads`);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
guards.push(g);
|
|
324
|
+
}
|
|
325
|
+
if (guards.length)
|
|
326
|
+
action.guards = guards;
|
|
327
|
+
// Affected concepts, deduplicated on the pair the loader rejects a repeat
|
|
328
|
+
// of, for the
|
|
329
|
+
// same reason a repeated guard is dropped here: keeping it would hand back a
|
|
330
|
+
// document that cannot be reloaded.
|
|
331
|
+
const affects = [];
|
|
332
|
+
const seen = new Set();
|
|
333
|
+
for (const raw of asArray(data.affects)) {
|
|
334
|
+
const affected = readAffectedConcept(raw, name, warnings);
|
|
335
|
+
if (!affected)
|
|
336
|
+
continue;
|
|
337
|
+
const key = `${affected.concept}/${affected.operation ?? ''}`;
|
|
338
|
+
if (seen.has(key)) {
|
|
339
|
+
warnings.push(`action '${name}': the ${kc_custom_types_1.ACTION_TYPE_ID} aspect repeats the entry ` +
|
|
340
|
+
`on '${affected.concept}'; the duplicate is dropped so the ` +
|
|
341
|
+
`document still loads`);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
seen.add(key);
|
|
345
|
+
affects.push(affected);
|
|
346
|
+
}
|
|
347
|
+
if (affects.length)
|
|
348
|
+
action.affects = affects;
|
|
349
|
+
const description = entry.entrySource?.description;
|
|
350
|
+
if (description !== undefined && description !== '')
|
|
351
|
+
action.description = description;
|
|
352
|
+
if (typeof data.instructions === 'string' && data.instructions !== '')
|
|
353
|
+
action.aiContext = { instructions: data.instructions };
|
|
354
|
+
return action;
|
|
355
|
+
}
|
|
356
|
+
// One action parameter from its aspect record. A record missing a name is
|
|
357
|
+
// dropped. `concept` and `field` come back together or not at all: half a
|
|
358
|
+
// projection is not something a re-push could state, so a record carrying only
|
|
359
|
+
// one of them recovers as the plain scalar it already is, and says so.
|
|
360
|
+
function readParameter(p, actionName, warnings) {
|
|
361
|
+
const name = typeof p?.name === 'string' ? p.name : '';
|
|
362
|
+
const type = typeof p?.type === 'string' ? p.type : '';
|
|
363
|
+
if (!name)
|
|
364
|
+
return undefined;
|
|
365
|
+
// An absent type comes back ABSENT, not as an empty string. `type` is
|
|
366
|
+
// required on the aspect, so a record without one is damage -- but the rest
|
|
367
|
+
// of the codebase reads `type === undefined` as "this parameter has no
|
|
368
|
+
// type", and a `''` would slip past every one of those checks while being
|
|
369
|
+
// no more of a datatype than `undefined` is.
|
|
370
|
+
const param = type ? { name, type } : { name };
|
|
371
|
+
const concept = typeof p?.concept === 'string' ? p.concept : '';
|
|
372
|
+
const field = typeof p?.field === 'string' ? p.field : '';
|
|
373
|
+
if (concept && field) {
|
|
374
|
+
param.concept = concept;
|
|
375
|
+
param.field = field;
|
|
376
|
+
}
|
|
377
|
+
else if (concept || field) {
|
|
378
|
+
warnings.push(`action '${actionName}': parameter '${name}' stores only ` +
|
|
379
|
+
`'${concept ? 'concept' : 'field'}' of a field projection; ` +
|
|
380
|
+
`recovered as a plain ${type || 'untyped'} parameter.`);
|
|
381
|
+
}
|
|
382
|
+
if (typeof p?.description === 'string' && p.description !== '') {
|
|
383
|
+
param.description = p.description;
|
|
384
|
+
}
|
|
385
|
+
if (typeof p?.required === 'boolean') {
|
|
386
|
+
param.required = p.required;
|
|
387
|
+
}
|
|
388
|
+
if (p?.default !== undefined && p.default !== '') {
|
|
389
|
+
param.default = parseDefaultFromAspect(p.default);
|
|
390
|
+
}
|
|
391
|
+
if (!type) {
|
|
392
|
+
warnings.push(`action '${actionName}': parameter '${name}' stores no type, which ` +
|
|
393
|
+
`the aspect requires; pulled without one`);
|
|
394
|
+
}
|
|
395
|
+
else if (!ir_1.DATA_TYPES.includes(type)) {
|
|
396
|
+
// Every parameter is a scalar now, so a type that is not one is damage
|
|
397
|
+
// rather than a reference this pull failed to resolve. Kept as stored --
|
|
398
|
+
// dropping it would hide what the catalog actually holds -- and reported,
|
|
399
|
+
// because a re-push will refuse it.
|
|
400
|
+
warnings.push(`action '${actionName}': parameter '${name}' type '${type}' is not a ` +
|
|
401
|
+
`scalar datatype; pulled as stored`);
|
|
402
|
+
}
|
|
403
|
+
return param;
|
|
404
|
+
}
|
|
405
|
+
function parseDefaultFromAspect(raw) {
|
|
406
|
+
if (typeof raw !== 'string')
|
|
407
|
+
return raw;
|
|
408
|
+
try {
|
|
409
|
+
const parsed = JSON.parse(raw);
|
|
410
|
+
if (typeof parsed === 'number' && String(parsed) !== raw) {
|
|
411
|
+
return raw;
|
|
412
|
+
}
|
|
413
|
+
return parsed;
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
return raw;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// One affected concept from its aspect record, the inverse of
|
|
420
|
+
// affectedConceptData. A record with no concept is dropped: it names nothing,
|
|
421
|
+
// so there is nothing to recover.
|
|
422
|
+
//
|
|
423
|
+
// Unlike readParameter, this resolves nothing against the pulled model. It
|
|
424
|
+
// could not do so honestly if it tried: a pull recovers relationships from the
|
|
425
|
+
// schema-join entry links, so a many-to-many edge is never among them, and
|
|
426
|
+
// every entry naming one would look unresolvable on a model that is perfectly
|
|
427
|
+
// well-formed. Nothing here needs the answer, so nothing asks.
|
|
428
|
+
function readAffectedConcept(raw, actionName, warnings) {
|
|
429
|
+
const concept = typeof raw?.concept === 'string' ? raw.concept : '';
|
|
430
|
+
if (!concept)
|
|
431
|
+
return undefined;
|
|
432
|
+
const affected = { concept };
|
|
433
|
+
if (ir_1.CONCEPT_OPERATIONS.includes(raw.operation)) {
|
|
434
|
+
affected.operation = raw.operation;
|
|
435
|
+
}
|
|
436
|
+
else if (raw.operation !== undefined && raw.operation !== '') {
|
|
437
|
+
// An aspect edited by hand can carry anything. Dropping the operation
|
|
438
|
+
// keeps the entry (the blast radius is still true) and says why.
|
|
439
|
+
warnings.push(`action '${actionName}': affects '${concept}' with an unknown ` +
|
|
440
|
+
`operation '${raw.operation}'; recovered with the operation dropped`);
|
|
441
|
+
}
|
|
442
|
+
const fields = [];
|
|
443
|
+
for (const f of asArray(raw.fields)) {
|
|
444
|
+
if (typeof f !== 'string' || f === '' || fields.includes(f))
|
|
445
|
+
continue;
|
|
446
|
+
fields.push(f);
|
|
447
|
+
}
|
|
448
|
+
if (fields.length)
|
|
449
|
+
affected.fields = fields;
|
|
450
|
+
return affected;
|
|
451
|
+
}
|
|
452
|
+
// The IR executor from the aspect's flat fields, the inverse of executorData.
|
|
453
|
+
// Returns undefined when the kind is unknown or either of its coordinates is
|
|
454
|
+
// missing.
|
|
455
|
+
function readExecutor(data) {
|
|
456
|
+
// A coordinate must be a present, NON-BLANK string. An aspect edited by hand
|
|
457
|
+
// can carry an empty one; treat that as malformed so the reader rejects it
|
|
458
|
+
// exactly as push-side validate would, rather than recovering an action the
|
|
459
|
+
// next push cannot deploy.
|
|
460
|
+
const str = (v) => typeof v === 'string' && v.trim() !== '';
|
|
461
|
+
switch (data.executorKind) {
|
|
462
|
+
case 'mcp':
|
|
463
|
+
if (str(data.mcpServer) && str(data.mcpTool))
|
|
464
|
+
return { kind: 'mcp', mcp: { server: data.mcpServer, tool: data.mcpTool } };
|
|
465
|
+
return undefined;
|
|
466
|
+
case 'rest':
|
|
467
|
+
if (str(data.restEndpoint) && str(data.restMethod))
|
|
468
|
+
return {
|
|
469
|
+
kind: 'rest',
|
|
470
|
+
rest: { endpoint: data.restEndpoint, method: data.restMethod }
|
|
471
|
+
};
|
|
472
|
+
return undefined;
|
|
473
|
+
case 'grpc':
|
|
474
|
+
if (str(data.grpcService) && str(data.grpcMethod))
|
|
475
|
+
return {
|
|
476
|
+
kind: 'grpc',
|
|
477
|
+
grpc: { service: data.grpcService, method: data.grpcMethod }
|
|
478
|
+
};
|
|
479
|
+
return undefined;
|
|
480
|
+
case 'sql': {
|
|
481
|
+
// Unlike the other three, the coordinate here is a list. A blank entry is
|
|
482
|
+
// dropped rather than kept: it is not a statement, and validate would
|
|
483
|
+
// reject the pulled model for carrying it. An executor left with no
|
|
484
|
+
// statement at all is malformed, like a missing coordinate elsewhere.
|
|
485
|
+
const statements = Array.isArray(data.sqlStatements) ?
|
|
486
|
+
data.sqlStatements.filter(str).map((s) => s.trim()) :
|
|
487
|
+
[];
|
|
488
|
+
if (statements.length)
|
|
489
|
+
return { kind: 'sql', sql: { statements } };
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
492
|
+
default:
|
|
493
|
+
return undefined;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
// ---------------------------------------------------------------------------
|
|
497
|
+
// Local helpers (see the file header on why they are not shared).
|
|
498
|
+
// ---------------------------------------------------------------------------
|
|
499
|
+
// The action aspect's `data` from an entry, matched by the aspect key's
|
|
500
|
+
// `.semantic-action` suffix or the aspectType's `/aspectTypes/semantic-action`
|
|
501
|
+
// suffix, so it is found whichever project the type was provisioned in.
|
|
502
|
+
function actionAspectDataOf(entry) {
|
|
503
|
+
for (const [key, aspect] of Object.entries(entry.aspects ?? {})) {
|
|
504
|
+
if (key.endsWith(`.${kc_custom_types_1.ACTION_TYPE_ID}`) ||
|
|
505
|
+
aspect.aspectType?.endsWith(`/aspectTypes/${kc_custom_types_1.ACTION_TYPE_ID}`)) {
|
|
506
|
+
return aspect.data ?? {};
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return {};
|
|
510
|
+
}
|
|
511
|
+
// Entry ids allow letters, numbers, underscores, hyphens, and periods.
|
|
512
|
+
function slug(s) {
|
|
513
|
+
return s.replace(/[^A-Za-z0-9_.-]/g, '_');
|
|
514
|
+
}
|
|
515
|
+
// The id segment of a full entry resource name.
|
|
516
|
+
function idOf(name) {
|
|
517
|
+
return name.split('/').pop() ?? name;
|
|
518
|
+
}
|
|
519
|
+
// Drops undefined-valued keys so the emitted aspect (and its golden) only shows
|
|
520
|
+
// fields the model actually set.
|
|
521
|
+
function compact(obj) {
|
|
522
|
+
const out = {};
|
|
523
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
524
|
+
if (v !== undefined)
|
|
525
|
+
out[k] = v;
|
|
526
|
+
}
|
|
527
|
+
return out;
|
|
528
|
+
}
|
|
529
|
+
function asArray(value) {
|
|
530
|
+
return Array.isArray(value) ? value : [];
|
|
531
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Entry } from '../gcp/dataplex';
|
|
2
|
+
import { Constraint, SemanticModel } from './ir';
|
|
3
|
+
export declare function constraintEntryTypeName(dest: {
|
|
4
|
+
project: string;
|
|
5
|
+
}): string;
|
|
6
|
+
export declare function constraintAspectTypeName(dest: {
|
|
7
|
+
project: string;
|
|
8
|
+
}): string;
|
|
9
|
+
export declare function constraintAspectKey(dest: {
|
|
10
|
+
project: string;
|
|
11
|
+
}): string;
|
|
12
|
+
export interface ConstraintEmitContext {
|
|
13
|
+
project: string;
|
|
14
|
+
entry(entryId: string): string;
|
|
15
|
+
anchor: string;
|
|
16
|
+
claim(entryId: string, label: string): boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function constraintEntryId(modelId: string, constraintName: string): string;
|
|
19
|
+
export declare function constraintOwnedPrefix(modelId: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* One entry per constraint, to append to the model's entries.
|
|
22
|
+
*
|
|
23
|
+
* Empty when the model declares no constraints, so a model without them is
|
|
24
|
+
* unchanged from before constraints existed. Warns once when it is non-empty:
|
|
25
|
+
* constraints reach Knowledge Catalog and nowhere else, which is worth saying
|
|
26
|
+
* out loud on a push that also deploys a graph.
|
|
27
|
+
*/
|
|
28
|
+
export declare function constraintEntries(model: SemanticModel, modelId: string, ctx: ConstraintEmitContext, warnings: string[]): Entry[];
|
|
29
|
+
export declare function isConstraintEntry(entry: Entry): boolean;
|
|
30
|
+
export declare function constraintAspectTypes(entryTypeBase: string): string[];
|
|
31
|
+
/**
|
|
32
|
+
* Recovers one constraint from its entry, the inverse of constraintEntries.
|
|
33
|
+
*
|
|
34
|
+
* Returns undefined, with a warning, for an entry that states no judgment: an
|
|
35
|
+
* invariant that states no rule would be pulled into a model that then fails
|
|
36
|
+
* its own push-side validation, so one bad entry degrades itself rather than
|
|
37
|
+
* the pull. An entry left over from a push that wrote the removed `expression`
|
|
38
|
+
* body lands there too, and the warning says so by name -- that catalog holds a
|
|
39
|
+
* rule this model can no longer state, and the author has to restate it.
|
|
40
|
+
*
|
|
41
|
+
* `evaluation` is not read. There is one body, so the word it carried says
|
|
42
|
+
* nothing the entry does not.
|
|
43
|
+
*/
|
|
44
|
+
export declare function readConstraint(entry: Entry, warnings: string[]): Constraint | undefined;
|