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