@causa/workspace-google 1.3.0 → 1.4.0-beta.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 +9 -0
- package/dist/configurations/generated.d.ts +3 -3
- package/dist/configurations/generated.js +8 -4
- package/dist/configurations/schemas/google.yaml +3 -3
- package/dist/functions/event-topic/create-backfill-source-bigquery.call.js +2 -2
- package/dist/functions/event-topic/query-events-bigquery.call.js +2 -2
- package/dist/functions/google-firestore/generate-typescript-decorators.js +2 -1
- package/dist/functions/google-pubsub/index.d.ts +1 -1
- package/dist/functions/google-pubsub/index.js +1 -1
- package/dist/functions/google-pubsub/write-topics.d.ts +7 -0
- package/dist/functions/google-pubsub/write-topics.js +10 -1
- package/dist/functions/google-spanner/generate-typescript-decorators.js +2 -1
- package/dist/functions/google-spanner/list-databases.d.ts +1 -1
- package/dist/functions/graph/index.d.ts +1 -0
- package/dist/functions/graph/index.js +1 -0
- package/dist/functions/graph/list-rules.d.ts +11 -0
- package/dist/functions/graph/list-rules.js +16 -0
- package/dist/functions/index.js +2 -1
- package/dist/graph/cloud-run.d.ts +98 -0
- package/dist/graph/cloud-run.js +102 -0
- package/dist/graph/ids.d.ts +35 -0
- package/dist/graph/ids.js +44 -0
- package/dist/graph/index.d.ts +6 -0
- package/dist/graph/index.js +6 -0
- package/dist/graph/rules/configuration/enqueues-assumed-from-google-task-trigger.d.ts +6 -0
- package/dist/graph/rules/configuration/enqueues-assumed-from-google-task-trigger.js +39 -0
- package/dist/graph/rules/configuration/firestore.d.ts +11 -0
- package/dist/graph/rules/configuration/firestore.js +93 -0
- package/dist/graph/rules/configuration/index.d.ts +5 -0
- package/dist/graph/rules/configuration/index.js +11 -0
- package/dist/graph/rules/configuration/service-container.d.ts +13 -0
- package/dist/graph/rules/configuration/service-container.js +123 -0
- package/dist/graph/rules/terraform/api-router.d.ts +15 -0
- package/dist/graph/rules/terraform/api-router.js +117 -0
- package/dist/graph/rules/terraform/cloud-run-cron-triggers.d.ts +11 -0
- package/dist/graph/rules/terraform/cloud-run-cron-triggers.js +67 -0
- package/dist/graph/rules/terraform/cloud-run-grants.d.ts +16 -0
- package/dist/graph/rules/terraform/cloud-run-grants.js +118 -0
- package/dist/graph/rules/terraform/cloud-run-module.d.ts +6 -0
- package/dist/graph/rules/terraform/cloud-run-module.js +29 -0
- package/dist/graph/rules/terraform/cloud-run-pubsub-triggers.d.ts +14 -0
- package/dist/graph/rules/terraform/cloud-run-pubsub-triggers.js +103 -0
- package/dist/graph/rules/terraform/cloud-run-services.d.ts +12 -0
- package/dist/graph/rules/terraform/cloud-run-services.js +61 -0
- package/dist/graph/rules/terraform/cloud-run-tasks-triggers.d.ts +14 -0
- package/dist/graph/rules/terraform/cloud-run-tasks-triggers.js +97 -0
- package/dist/graph/rules/terraform/event-topics.d.ts +18 -0
- package/dist/graph/rules/terraform/event-topics.js +193 -0
- package/dist/graph/rules/terraform/index.d.ts +5 -0
- package/dist/graph/rules/terraform/index.js +13 -0
- package/dist/graph/rules/terraform/spanner-databases.d.ts +17 -0
- package/dist/graph/rules/terraform/spanner-databases.js +180 -0
- package/dist/graph/spanner.d.ts +106 -0
- package/dist/graph/spanner.js +118 -0
- package/dist/graph/terraform-modules.d.ts +87 -0
- package/dist/graph/terraform-modules.js +107 -0
- package/package.json +27 -26
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { GraphOriginKind, } from '@causa/workspace-core';
|
|
2
|
+
import { databaseId, tableId, tableLocator, DomainsFact, ModelFact, nodeId, } from '@causa/workspace-core/graph';
|
|
3
|
+
import { terraformDeploys } from '@causa/workspace-terraform';
|
|
4
|
+
import { SPANNER_ENGINE, spannerInstanceId } from '../../ids.js';
|
|
5
|
+
import { ddlSource, resolveSpannerTable, SpannerFact } from '../../spanner.js';
|
|
6
|
+
import { blockResource, infrastructureModules, argumentOrConfiguration, } from '../../terraform-modules.js';
|
|
7
|
+
/**
|
|
8
|
+
* The source of the Causa Terraform module creating the Spanner instance and databases.
|
|
9
|
+
*/
|
|
10
|
+
export const SPANNER_DATABASES_MODULE_SOURCE = 'causa-io/spanner-databases/google';
|
|
11
|
+
/**
|
|
12
|
+
* Returns the GCP project and instance parts of the resource names created by a Spanner databases module block.
|
|
13
|
+
*/
|
|
14
|
+
function nameParts(module, warnings) {
|
|
15
|
+
return {
|
|
16
|
+
project: argumentOrConfiguration(module.block, 'gcp_project_id', 'google.project', warnings),
|
|
17
|
+
instance: argumentOrConfiguration(module.block, 'instance_name', 'google.spanner.instance.name', warnings),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The resources created by the blocks of the Spanner databases module:
|
|
22
|
+
*
|
|
23
|
+
* - A `google.spanner.instance` node per block, which creates the instance from `google.spanner.instance.name`.
|
|
24
|
+
* - A `database` node per Spanner database, as `GoogleSpannerListDatabases` lists them. The locator is
|
|
25
|
+
* `google.spanner/<instance>.<database>`, and the parent is the instance.
|
|
26
|
+
* - A `table` node per `CREATE TABLE` statement in the DDL files of each database. Tables no schema declares (e.g. an
|
|
27
|
+
* outbox) are still nodes, realizing nothing.
|
|
28
|
+
* - A `realizes` edge from a table to the entity whose binding names it. The table is the one created by the DDL of a
|
|
29
|
+
* database, disambiguated by domain when several databases create a table with the same name.
|
|
30
|
+
*/
|
|
31
|
+
export const spannerDatabasesModule = {
|
|
32
|
+
name: 'spannerDatabasesModule',
|
|
33
|
+
kind: GraphOriginKind.Declared,
|
|
34
|
+
description: 'One `google.spanner.instance` node per Spanner databases module block, named by `google.spanner.instance.name`. One `database` node per Spanner database listed by `GoogleSpannerListDatabases` from the DDL files, and one `table` node per `CREATE TABLE` in them, realizing the entity whose `googleSpannerTable` binding names it, disambiguated by domain. Each resource is linked by a `deploys` edge from the projects applying the block.',
|
|
35
|
+
async run(graph) {
|
|
36
|
+
const { locator, context } = graph;
|
|
37
|
+
const [domains, model, spanner, spannerDatabases] = await Promise.all([
|
|
38
|
+
graph.get(DomainsFact),
|
|
39
|
+
graph.get(ModelFact),
|
|
40
|
+
graph.get(SpannerFact),
|
|
41
|
+
infrastructureModules(graph, SPANNER_DATABASES_MODULE_SOURCE),
|
|
42
|
+
]);
|
|
43
|
+
const nodes = [];
|
|
44
|
+
const edges = [];
|
|
45
|
+
const warnings = [];
|
|
46
|
+
for (const module of spannerDatabases) {
|
|
47
|
+
const source = module.block.declaration;
|
|
48
|
+
if (!spanner.instance) {
|
|
49
|
+
warnings.push({
|
|
50
|
+
message: "A Spanner databases module block exists, but 'google.spanner.instance.name' is not set.",
|
|
51
|
+
sources: [source],
|
|
52
|
+
});
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const { project, instance } = nameParts(module, warnings);
|
|
56
|
+
if (module.block.arguments['instance_name'] !== undefined) {
|
|
57
|
+
warnings.push({
|
|
58
|
+
message: `The module overrides the instance name. Locators use the configured '${spanner.instance}'.`,
|
|
59
|
+
sources: [source],
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
nodes.push({
|
|
63
|
+
layer: 'infrastructure',
|
|
64
|
+
type: 'google.spanner.instance',
|
|
65
|
+
locator: spanner.instance,
|
|
66
|
+
name: spanner.instance,
|
|
67
|
+
sources: [
|
|
68
|
+
await locator.configurationSource(module.context, [
|
|
69
|
+
'google',
|
|
70
|
+
'spanner',
|
|
71
|
+
'instance',
|
|
72
|
+
'name',
|
|
73
|
+
]),
|
|
74
|
+
source,
|
|
75
|
+
],
|
|
76
|
+
data: {
|
|
77
|
+
resource: await blockResource(graph, module.block, {
|
|
78
|
+
type: 'spanner.googleapis.com/Instance',
|
|
79
|
+
scope: module.project,
|
|
80
|
+
id: ['projects/', project, '/instances/', instance],
|
|
81
|
+
}, warnings),
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
edges.push(...terraformDeploys(module.block, nodeId('google.spanner.instance', spanner.instance)));
|
|
85
|
+
for (const database of spanner.databases) {
|
|
86
|
+
const data = {
|
|
87
|
+
engine: SPANNER_ENGINE,
|
|
88
|
+
resource: await blockResource(graph, module.block, {
|
|
89
|
+
type: 'spanner.googleapis.com/Database',
|
|
90
|
+
scope: module.project,
|
|
91
|
+
id: [
|
|
92
|
+
'projects/',
|
|
93
|
+
project,
|
|
94
|
+
'/instances/',
|
|
95
|
+
instance,
|
|
96
|
+
`/databases/${database.id}`,
|
|
97
|
+
],
|
|
98
|
+
}, warnings),
|
|
99
|
+
};
|
|
100
|
+
nodes.push({
|
|
101
|
+
layer: 'infrastructure',
|
|
102
|
+
type: 'database',
|
|
103
|
+
locator: database.locator,
|
|
104
|
+
parent: spannerInstanceId(spanner.instance),
|
|
105
|
+
name: database.id,
|
|
106
|
+
sources: [
|
|
107
|
+
{ path: database.directory },
|
|
108
|
+
await locator.configurationSource(context, [
|
|
109
|
+
'google',
|
|
110
|
+
'spanner',
|
|
111
|
+
'ddls',
|
|
112
|
+
]),
|
|
113
|
+
source,
|
|
114
|
+
],
|
|
115
|
+
data,
|
|
116
|
+
});
|
|
117
|
+
edges.push(...terraformDeploys(module.block, nodeId('database', database.locator)));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (spanner.databases.length > 0 && spannerDatabases.length === 0) {
|
|
121
|
+
warnings.push({
|
|
122
|
+
message: 'Spanner DDL files exist but no Spanner databases module block creates the databases.',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (!spanner.instance || spannerDatabases.length === 0) {
|
|
126
|
+
return { nodes, edges, warnings };
|
|
127
|
+
}
|
|
128
|
+
const blocks = spannerDatabases.map((m) => m.block.declaration);
|
|
129
|
+
for (const database of spanner.databases) {
|
|
130
|
+
for (const table of database.tables) {
|
|
131
|
+
const nodeLocator = tableLocator(database.locator, table.name);
|
|
132
|
+
nodes.push({
|
|
133
|
+
layer: 'infrastructure',
|
|
134
|
+
type: 'table',
|
|
135
|
+
locator: nodeLocator,
|
|
136
|
+
parent: databaseId(database.locator),
|
|
137
|
+
name: table.name,
|
|
138
|
+
sources: [ddlSource(table), ...blocks],
|
|
139
|
+
});
|
|
140
|
+
edges.push(...spannerDatabases.flatMap((module) => terraformDeploys(module.block, nodeId('table', nodeLocator))));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const entity of model.entities.values()) {
|
|
144
|
+
const resolution = resolveSpannerTable(spanner, entity, domains);
|
|
145
|
+
if (!resolution) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const { file, segments, definition } = entity.schema;
|
|
149
|
+
const binding = await locator.source(file, [
|
|
150
|
+
...segments,
|
|
151
|
+
'causa',
|
|
152
|
+
'googleSpannerTable',
|
|
153
|
+
]);
|
|
154
|
+
const table = definition.databases.find((d) => d.engine === SPANNER_ENGINE)?.table;
|
|
155
|
+
if (resolution.kind === 'missing') {
|
|
156
|
+
warnings.push({
|
|
157
|
+
message: `Entity '${definition.name}' binds to Spanner table '${table}', which no DDL creates.`,
|
|
158
|
+
sources: [binding],
|
|
159
|
+
});
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (resolution.kind === 'ambiguous') {
|
|
163
|
+
warnings.push({
|
|
164
|
+
message: `Entity '${definition.name}' binds to Spanner table '${table}', which is created in several databases: ${resolution.candidates
|
|
165
|
+
.map((c) => `'${c.id}'`)
|
|
166
|
+
.join(', ')}.`,
|
|
167
|
+
sources: [binding],
|
|
168
|
+
});
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
edges.push({
|
|
172
|
+
type: 'realizes',
|
|
173
|
+
from: tableId(resolution.database.locator, resolution.table.name),
|
|
174
|
+
to: entity.id,
|
|
175
|
+
sources: [binding, ddlSource(resolution.table)],
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return { nodes, edges, warnings };
|
|
179
|
+
},
|
|
180
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { GraphOriginSource } from '@causa/workspace-core';
|
|
2
|
+
import { GraphFact, type EntityFacts, type GraphContext, type GraphFactOutput, type WorkspaceDomain } from '@causa/workspace-core/graph';
|
|
3
|
+
/**
|
|
4
|
+
* A table created by a `CREATE TABLE` statement in a Spanner DDL file.
|
|
5
|
+
*/
|
|
6
|
+
export type SpannerTable = {
|
|
7
|
+
/**
|
|
8
|
+
* The name of the table.
|
|
9
|
+
*/
|
|
10
|
+
readonly name: string;
|
|
11
|
+
/**
|
|
12
|
+
* The DDL file declaring the table, relative to the workspace root.
|
|
13
|
+
*/
|
|
14
|
+
readonly file: string;
|
|
15
|
+
/**
|
|
16
|
+
* The line of the statement in the file.
|
|
17
|
+
*/
|
|
18
|
+
readonly line: number;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* A Spanner database of the workspace, and the tables its DDL files create.
|
|
22
|
+
*/
|
|
23
|
+
export type SpannerDatabaseFacts = {
|
|
24
|
+
/**
|
|
25
|
+
* The ID of the database, as `GoogleSpannerListDatabases` returns it.
|
|
26
|
+
*/
|
|
27
|
+
readonly id: string;
|
|
28
|
+
/**
|
|
29
|
+
* The name of the database within its engine, `<instance>.<database>`, as `outputs['google.spanner']` lists it.
|
|
30
|
+
*/
|
|
31
|
+
readonly name: string;
|
|
32
|
+
/**
|
|
33
|
+
* The locator of the `database` node.
|
|
34
|
+
*/
|
|
35
|
+
readonly locator: string;
|
|
36
|
+
/**
|
|
37
|
+
* The directory holding the DDL files, relative to the workspace root.
|
|
38
|
+
*/
|
|
39
|
+
readonly directory: string;
|
|
40
|
+
/**
|
|
41
|
+
* The domain the DDL files belong to.
|
|
42
|
+
*/
|
|
43
|
+
readonly domain: WorkspaceDomain | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* The tables created by the DDL files.
|
|
46
|
+
*/
|
|
47
|
+
readonly tables: SpannerTable[];
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The Spanner databases of the workspace.
|
|
51
|
+
*/
|
|
52
|
+
export type SpannerFacts = {
|
|
53
|
+
/**
|
|
54
|
+
* The Spanner instance, from `google.spanner.instance.name`.
|
|
55
|
+
*/
|
|
56
|
+
readonly instance: string | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* The databases, as `GoogleSpannerListDatabases` lists them.
|
|
59
|
+
*/
|
|
60
|
+
readonly databases: SpannerDatabaseFacts[];
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* The Spanner databases of the workspace, as `GoogleSpannerListDatabases` lists them, and the tables their DDL files
|
|
64
|
+
* create.
|
|
65
|
+
*/
|
|
66
|
+
export declare class SpannerFact extends GraphFact<SpannerFacts> {
|
|
67
|
+
compute(graph: GraphContext): Promise<GraphFactOutput<SpannerFacts>>;
|
|
68
|
+
/**
|
|
69
|
+
* Parses the tables created by the `CREATE TABLE` statements of DDL files.
|
|
70
|
+
*
|
|
71
|
+
* @param context The workspace context.
|
|
72
|
+
* @param files The DDL files, relative to the workspace root.
|
|
73
|
+
* @returns The tables, in the order of the files and statements.
|
|
74
|
+
*/
|
|
75
|
+
private parseTables;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The resolution of an entity's Spanner binding to a table.
|
|
79
|
+
*/
|
|
80
|
+
export type SpannerTableResolution = {
|
|
81
|
+
readonly kind: 'resolved';
|
|
82
|
+
readonly database: SpannerDatabaseFacts;
|
|
83
|
+
readonly table: SpannerTable;
|
|
84
|
+
} | {
|
|
85
|
+
readonly kind: 'ambiguous';
|
|
86
|
+
readonly candidates: SpannerDatabaseFacts[];
|
|
87
|
+
} | {
|
|
88
|
+
readonly kind: 'missing';
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Resolves the Spanner binding of an entity to the database whose DDL creates a table of that name. When several
|
|
92
|
+
* databases declare the name, the one in the entity's domain is chosen.
|
|
93
|
+
*
|
|
94
|
+
* @param spanner The Spanner databases.
|
|
95
|
+
* @param entity The entity.
|
|
96
|
+
* @param domains The domains of the workspace, in which the domain of the entity is looked up.
|
|
97
|
+
* @returns The resolution, or `undefined` if the entity is not persisted in Spanner.
|
|
98
|
+
*/
|
|
99
|
+
export declare function resolveSpannerTable(spanner: SpannerFacts, entity: EntityFacts, domains: readonly WorkspaceDomain[]): SpannerTableResolution | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* Returns the origin source of a `CREATE TABLE` statement.
|
|
102
|
+
*
|
|
103
|
+
* @param table The table.
|
|
104
|
+
* @returns The origin source.
|
|
105
|
+
*/
|
|
106
|
+
export declare function ddlSource(table: SpannerTable): GraphOriginSource;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { domainAt, domainOfFile, DomainsFact, GraphFact, } from '@causa/workspace-core/graph';
|
|
2
|
+
import { readFile } from 'fs/promises';
|
|
3
|
+
import { dirname, join, relative } from 'path';
|
|
4
|
+
import { GoogleSpannerListDatabases } from '../functions/google-spanner/index.js';
|
|
5
|
+
import { SPANNER_ENGINE, spannerDatabaseLocator, spannerDatabaseName, } from './ids.js';
|
|
6
|
+
/**
|
|
7
|
+
* The Spanner databases of the workspace, as `GoogleSpannerListDatabases` lists them, and the tables their DDL files
|
|
8
|
+
* create.
|
|
9
|
+
*/
|
|
10
|
+
export class SpannerFact extends GraphFact {
|
|
11
|
+
async compute(graph) {
|
|
12
|
+
const { context } = graph;
|
|
13
|
+
const instance = context
|
|
14
|
+
.asConfiguration()
|
|
15
|
+
.get('google.spanner.instance.name');
|
|
16
|
+
const [listed, domains] = await Promise.all([
|
|
17
|
+
context.call(GoogleSpannerListDatabases, {}),
|
|
18
|
+
graph.get(DomainsFact),
|
|
19
|
+
]);
|
|
20
|
+
const databases = await Promise.all(listed.map(async ({ id, ddlFiles }) => {
|
|
21
|
+
const files = ddlFiles.map((f) => relative(context.rootPath, f)).sort();
|
|
22
|
+
const tables = await this.parseTables(context, files);
|
|
23
|
+
const directory = files[0] ? dirname(files[0]) : '';
|
|
24
|
+
return {
|
|
25
|
+
id,
|
|
26
|
+
name: spannerDatabaseName(instance ?? '', id),
|
|
27
|
+
locator: spannerDatabaseLocator(instance ?? '', id),
|
|
28
|
+
directory,
|
|
29
|
+
domain: directory ? domainAt(domains, directory) : undefined,
|
|
30
|
+
tables,
|
|
31
|
+
};
|
|
32
|
+
}));
|
|
33
|
+
databases.sort((a, b) => a.id.localeCompare(b.id));
|
|
34
|
+
const warnings = [];
|
|
35
|
+
for (const database of databases) {
|
|
36
|
+
const names = new Set();
|
|
37
|
+
for (const table of database.tables) {
|
|
38
|
+
if (names.has(table.name)) {
|
|
39
|
+
warnings.push({
|
|
40
|
+
message: `Table '${table.name}' is created twice in database '${database.id}'.`,
|
|
41
|
+
sources: [ddlSource(table)],
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
names.add(table.name);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { value: { instance, databases }, warnings };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Parses the tables created by the `CREATE TABLE` statements of DDL files.
|
|
51
|
+
*
|
|
52
|
+
* @param context The workspace context.
|
|
53
|
+
* @param files The DDL files, relative to the workspace root.
|
|
54
|
+
* @returns The tables, in the order of the files and statements.
|
|
55
|
+
*/
|
|
56
|
+
async parseTables(context, files) {
|
|
57
|
+
const tables = [];
|
|
58
|
+
for (const file of files) {
|
|
59
|
+
const content = await readFile(join(context.rootPath, file), 'utf-8');
|
|
60
|
+
const withoutComments = content.replace(/--.*$/gm, (m) => ' '.repeat(m.length));
|
|
61
|
+
const statements = withoutComments.matchAll(/\bCREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"']?([A-Za-z_]\w*)[`"']?/gi);
|
|
62
|
+
for (const match of statements) {
|
|
63
|
+
tables.push({
|
|
64
|
+
name: match[1],
|
|
65
|
+
file,
|
|
66
|
+
line: withoutComments.slice(0, match.index).split('\n').length,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return tables;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolves the Spanner binding of an entity to the database whose DDL creates a table of that name. When several
|
|
75
|
+
* databases declare the name, the one in the entity's domain is chosen.
|
|
76
|
+
*
|
|
77
|
+
* @param spanner The Spanner databases.
|
|
78
|
+
* @param entity The entity.
|
|
79
|
+
* @param domains The domains of the workspace, in which the domain of the entity is looked up.
|
|
80
|
+
* @returns The resolution, or `undefined` if the entity is not persisted in Spanner.
|
|
81
|
+
*/
|
|
82
|
+
export function resolveSpannerTable(spanner, entity, domains) {
|
|
83
|
+
const { definition, file } = entity.schema;
|
|
84
|
+
const table = definition.databases.find((d) => d.engine === SPANNER_ENGINE)?.table;
|
|
85
|
+
if (!table) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
const candidates = spanner.databases.filter((d) => d.tables.some((t) => t.name === table));
|
|
89
|
+
const pick = (database) => ({
|
|
90
|
+
kind: 'resolved',
|
|
91
|
+
database,
|
|
92
|
+
table: database.tables.find((t) => t.name === table),
|
|
93
|
+
});
|
|
94
|
+
if (candidates.length === 1) {
|
|
95
|
+
return pick(candidates[0]);
|
|
96
|
+
}
|
|
97
|
+
if (candidates.length === 0) {
|
|
98
|
+
return { kind: 'missing' };
|
|
99
|
+
}
|
|
100
|
+
const domain = domainOfFile(domains, file);
|
|
101
|
+
const sameDomain = candidates.filter((d) => domain && d.domain?.directory === domain.directory);
|
|
102
|
+
return sameDomain.length === 1
|
|
103
|
+
? pick(sameDomain[0])
|
|
104
|
+
: { kind: 'ambiguous', candidates };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Returns the origin source of a `CREATE TABLE` statement.
|
|
108
|
+
*
|
|
109
|
+
* @param table The table.
|
|
110
|
+
* @returns The origin source.
|
|
111
|
+
*/
|
|
112
|
+
export function ddlSource(table) {
|
|
113
|
+
return {
|
|
114
|
+
path: table.file,
|
|
115
|
+
pointer: `CREATE TABLE ${table.name}`,
|
|
116
|
+
location: { start: { line: table.line } },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { WorkspaceContext } from '@causa/workspace';
|
|
2
|
+
import { type GraphContext, type GraphResource, type GraphTemplatePart, type GraphWarning, type WorkspaceProject } from '@causa/workspace-core/graph';
|
|
3
|
+
import { type TerraformArgument, type TerraformBlock, type TerraformModuleBlock } from '@causa/workspace-terraform';
|
|
4
|
+
/**
|
|
5
|
+
* Finds the project whose configuration file a block reads. The argument is a path ending with `<project name>.json`,
|
|
6
|
+
* the file written by `ProjectWriteConfigurations`.
|
|
7
|
+
*
|
|
8
|
+
* @param argument The argument of the module block referencing the configuration file.
|
|
9
|
+
* @param projects The projects of the workspace.
|
|
10
|
+
* @returns The project, or `undefined` if the argument does not name a project of the workspace.
|
|
11
|
+
*/
|
|
12
|
+
export declare function projectOfConfigurationFile(argument: TerraformArgument | undefined, projects: readonly WorkspaceProject[]): WorkspaceProject | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* A block of a workspace-level module, which reads the configuration of an infrastructure project
|
|
15
|
+
* (`infrastructure_configuration_file`).
|
|
16
|
+
*/
|
|
17
|
+
export type InfrastructureModule = {
|
|
18
|
+
/**
|
|
19
|
+
* The module block.
|
|
20
|
+
*/
|
|
21
|
+
readonly block: TerraformModuleBlock;
|
|
22
|
+
/**
|
|
23
|
+
* The infrastructure project whose configuration the module reads.
|
|
24
|
+
*/
|
|
25
|
+
readonly project: WorkspaceProject | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* The context whose configuration the module reads: the project's if it is found, otherwise the workspace's.
|
|
28
|
+
*/
|
|
29
|
+
readonly context: WorkspaceContext;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Returns the blocks of a workspace-level module, along with the context whose configuration each reads. The workspace
|
|
33
|
+
* context is used when the infrastructure project cannot be found.
|
|
34
|
+
*
|
|
35
|
+
* @param graph The context of the extraction.
|
|
36
|
+
* @param source The source of the module, e.g. `causa-io/event-topics-pubsub/google`.
|
|
37
|
+
* @returns The blocks of the module.
|
|
38
|
+
*/
|
|
39
|
+
export declare function infrastructureModules(graph: GraphContext, source: string): Promise<InfrastructureModule[]>;
|
|
40
|
+
/**
|
|
41
|
+
* Returns a string argument of a module block, or a reference to the configuration the module falls back to, mirroring
|
|
42
|
+
* the `coalesce(var.<argument>, <configuration>)` logic of the Causa Terraform modules. When the block passes the
|
|
43
|
+
* argument but it cannot be evaluated as a string, the configuration is assumed, and a warning is raised.
|
|
44
|
+
*
|
|
45
|
+
* @param block The module block.
|
|
46
|
+
* @param argument The name of the module variable.
|
|
47
|
+
* @param configuration The path to the configuration value the module falls back to.
|
|
48
|
+
* @param warnings The warnings to which the one about a computed argument is added.
|
|
49
|
+
* @returns The literal value of the argument, or a reference to the configuration.
|
|
50
|
+
*/
|
|
51
|
+
export declare function argumentOrConfiguration(block: TerraformModuleBlock, argument: string, configuration: string, warnings: GraphWarning[]): GraphTemplatePart;
|
|
52
|
+
/**
|
|
53
|
+
* The infrastructure resource created by a Terraform block, before its identifier is built.
|
|
54
|
+
*/
|
|
55
|
+
export type BlockResource = {
|
|
56
|
+
/**
|
|
57
|
+
* The type of the resource, e.g. `pubsub.googleapis.com/Topic`.
|
|
58
|
+
*/
|
|
59
|
+
readonly type: string;
|
|
60
|
+
/**
|
|
61
|
+
* The project whose configuration the block reads, if any. Otherwise, the workspace configuration is used.
|
|
62
|
+
*/
|
|
63
|
+
readonly scope?: WorkspaceProject;
|
|
64
|
+
/**
|
|
65
|
+
* The parts of the identifier of the resource.
|
|
66
|
+
*/
|
|
67
|
+
readonly id: GraphTemplatePart[];
|
|
68
|
+
/**
|
|
69
|
+
* Whether {@link BlockResource.id} is only a prefix of the identifier, when the full identifier cannot be known from
|
|
70
|
+
* the workspace.
|
|
71
|
+
*/
|
|
72
|
+
readonly prefix?: boolean;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Returns the infrastructure resource a Terraform block creates.
|
|
76
|
+
* A block applied by the environment project creates the resource once per environment: its identifier references
|
|
77
|
+
* the configuration, to be rendered for a given environment, using the configuration of the scope. Other
|
|
78
|
+
* infrastructure projects do not depend on the environment: the identifier is rendered, and the resource has no
|
|
79
|
+
* scope. If it cannot be rendered, a warning is raised and the identifier references the configuration.
|
|
80
|
+
*
|
|
81
|
+
* @param graph The context of the extraction.
|
|
82
|
+
* @param block The block creating the resource.
|
|
83
|
+
* @param resource The resource.
|
|
84
|
+
* @param warnings The warnings to which the one about an identifier that cannot be rendered is added.
|
|
85
|
+
* @returns The {@link GraphResource}.
|
|
86
|
+
*/
|
|
87
|
+
export declare function blockResource(graph: GraphContext, block: TerraformBlock, resource: BlockResource, warnings: GraphWarning[]): Promise<GraphResource>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { graphTemplate, projectId, ProjectsFact, renderGraphTemplate, } from '@causa/workspace-core/graph';
|
|
2
|
+
import { modulesWithSource, TerraformProjectsFact, } from '@causa/workspace-terraform';
|
|
3
|
+
import { basename } from 'path';
|
|
4
|
+
/**
|
|
5
|
+
* Finds the project whose configuration file a block reads. The argument is a path ending with `<project name>.json`,
|
|
6
|
+
* the file written by `ProjectWriteConfigurations`.
|
|
7
|
+
*
|
|
8
|
+
* @param argument The argument of the module block referencing the configuration file.
|
|
9
|
+
* @param projects The projects of the workspace.
|
|
10
|
+
* @returns The project, or `undefined` if the argument does not name a project of the workspace.
|
|
11
|
+
*/
|
|
12
|
+
export function projectOfConfigurationFile(argument, projects) {
|
|
13
|
+
if (argument === undefined) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const path = 'value' in argument ? argument.value : argument.expression;
|
|
17
|
+
if (typeof path !== 'string') {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const file = basename(path);
|
|
21
|
+
if (!/^[^${}]+\.json$/.test(file)) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const name = file.slice(0, -'.json'.length);
|
|
25
|
+
return projects.find((p) => p.name === name);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Returns the blocks of a workspace-level module, along with the context whose configuration each reads. The workspace
|
|
29
|
+
* context is used when the infrastructure project cannot be found.
|
|
30
|
+
*
|
|
31
|
+
* @param graph The context of the extraction.
|
|
32
|
+
* @param source The source of the module, e.g. `causa-io/event-topics-pubsub/google`.
|
|
33
|
+
* @returns The blocks of the module.
|
|
34
|
+
*/
|
|
35
|
+
export async function infrastructureModules(graph, source) {
|
|
36
|
+
const [terraformProjects, projects] = await Promise.all([
|
|
37
|
+
graph.get(TerraformProjectsFact),
|
|
38
|
+
graph.get(ProjectsFact),
|
|
39
|
+
]);
|
|
40
|
+
return modulesWithSource(terraformProjects, source).map((block) => {
|
|
41
|
+
const project = projectOfConfigurationFile(block.arguments['infrastructure_configuration_file'], projects);
|
|
42
|
+
return { block, project, context: project?.context ?? graph.context };
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Returns a string argument of a module block, or a reference to the configuration the module falls back to, mirroring
|
|
47
|
+
* the `coalesce(var.<argument>, <configuration>)` logic of the Causa Terraform modules. When the block passes the
|
|
48
|
+
* argument but it cannot be evaluated as a string, the configuration is assumed, and a warning is raised.
|
|
49
|
+
*
|
|
50
|
+
* @param block The module block.
|
|
51
|
+
* @param argument The name of the module variable.
|
|
52
|
+
* @param configuration The path to the configuration value the module falls back to.
|
|
53
|
+
* @param warnings The warnings to which the one about a computed argument is added.
|
|
54
|
+
* @returns The literal value of the argument, or a reference to the configuration.
|
|
55
|
+
*/
|
|
56
|
+
export function argumentOrConfiguration(block, argument, configuration, warnings) {
|
|
57
|
+
const value = block.arguments[argument];
|
|
58
|
+
if (value === undefined ||
|
|
59
|
+
('value' in value && (value.value === null || value.value === ''))) {
|
|
60
|
+
return { configuration };
|
|
61
|
+
}
|
|
62
|
+
if ('value' in value && typeof value.value === 'string') {
|
|
63
|
+
return value.value;
|
|
64
|
+
}
|
|
65
|
+
const expression = 'expression' in value ? value.expression : JSON.stringify(value.value);
|
|
66
|
+
warnings.push({
|
|
67
|
+
message: `The argument '${argument}' is computed (${expression}). Assuming the '${configuration}' configuration.`,
|
|
68
|
+
sources: [block.declaration],
|
|
69
|
+
});
|
|
70
|
+
return { configuration };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Returns the infrastructure resource a Terraform block creates.
|
|
74
|
+
* A block applied by the environment project creates the resource once per environment: its identifier references
|
|
75
|
+
* the configuration, to be rendered for a given environment, using the configuration of the scope. Other
|
|
76
|
+
* infrastructure projects do not depend on the environment: the identifier is rendered, and the resource has no
|
|
77
|
+
* scope. If it cannot be rendered, a warning is raised and the identifier references the configuration.
|
|
78
|
+
*
|
|
79
|
+
* @param graph The context of the extraction.
|
|
80
|
+
* @param block The block creating the resource.
|
|
81
|
+
* @param resource The resource.
|
|
82
|
+
* @param warnings The warnings to which the one about an identifier that cannot be rendered is added.
|
|
83
|
+
* @returns The {@link GraphResource}.
|
|
84
|
+
*/
|
|
85
|
+
export async function blockResource(graph, block, resource, warnings) {
|
|
86
|
+
const { type, scope, id, prefix } = resource;
|
|
87
|
+
const build = (id, scope) => ({
|
|
88
|
+
type,
|
|
89
|
+
...(prefix ? { idPrefix: id } : { id }),
|
|
90
|
+
...(scope ? { scope } : {}),
|
|
91
|
+
});
|
|
92
|
+
const templated = build(graphTemplate(...id), scope && projectId(scope.directory));
|
|
93
|
+
if (block.module.project.environment) {
|
|
94
|
+
return templated;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const context = scope?.context ?? graph.context;
|
|
98
|
+
return build(await renderGraphTemplate(context, ...id));
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
warnings.push({
|
|
102
|
+
message: `The identifier of the '${type}' resource cannot be rendered, although the block is not applied by the environment project: ${error.message}`,
|
|
103
|
+
sources: [block.declaration],
|
|
104
|
+
});
|
|
105
|
+
return templated;
|
|
106
|
+
}
|
|
107
|
+
}
|