@causa/workspace-google 1.3.1 → 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.
Files changed (54) hide show
  1. package/README.md +9 -0
  2. package/dist/functions/event-topic/create-backfill-source-bigquery.call.js +2 -2
  3. package/dist/functions/event-topic/query-events-bigquery.call.js +2 -2
  4. package/dist/functions/google-firestore/generate-typescript-decorators.js +2 -1
  5. package/dist/functions/google-pubsub/index.d.ts +1 -1
  6. package/dist/functions/google-pubsub/index.js +1 -1
  7. package/dist/functions/google-pubsub/write-topics.d.ts +7 -0
  8. package/dist/functions/google-pubsub/write-topics.js +10 -1
  9. package/dist/functions/google-spanner/generate-typescript-decorators.js +2 -1
  10. package/dist/functions/google-spanner/list-databases.d.ts +1 -1
  11. package/dist/functions/graph/index.d.ts +1 -0
  12. package/dist/functions/graph/index.js +1 -0
  13. package/dist/functions/graph/list-rules.d.ts +11 -0
  14. package/dist/functions/graph/list-rules.js +16 -0
  15. package/dist/functions/index.js +2 -1
  16. package/dist/graph/cloud-run.d.ts +98 -0
  17. package/dist/graph/cloud-run.js +102 -0
  18. package/dist/graph/ids.d.ts +35 -0
  19. package/dist/graph/ids.js +44 -0
  20. package/dist/graph/index.d.ts +6 -0
  21. package/dist/graph/index.js +6 -0
  22. package/dist/graph/rules/configuration/enqueues-assumed-from-google-task-trigger.d.ts +6 -0
  23. package/dist/graph/rules/configuration/enqueues-assumed-from-google-task-trigger.js +39 -0
  24. package/dist/graph/rules/configuration/firestore.d.ts +11 -0
  25. package/dist/graph/rules/configuration/firestore.js +93 -0
  26. package/dist/graph/rules/configuration/index.d.ts +5 -0
  27. package/dist/graph/rules/configuration/index.js +11 -0
  28. package/dist/graph/rules/configuration/service-container.d.ts +13 -0
  29. package/dist/graph/rules/configuration/service-container.js +123 -0
  30. package/dist/graph/rules/terraform/api-router.d.ts +15 -0
  31. package/dist/graph/rules/terraform/api-router.js +117 -0
  32. package/dist/graph/rules/terraform/cloud-run-cron-triggers.d.ts +11 -0
  33. package/dist/graph/rules/terraform/cloud-run-cron-triggers.js +67 -0
  34. package/dist/graph/rules/terraform/cloud-run-grants.d.ts +16 -0
  35. package/dist/graph/rules/terraform/cloud-run-grants.js +118 -0
  36. package/dist/graph/rules/terraform/cloud-run-module.d.ts +6 -0
  37. package/dist/graph/rules/terraform/cloud-run-module.js +29 -0
  38. package/dist/graph/rules/terraform/cloud-run-pubsub-triggers.d.ts +14 -0
  39. package/dist/graph/rules/terraform/cloud-run-pubsub-triggers.js +103 -0
  40. package/dist/graph/rules/terraform/cloud-run-services.d.ts +12 -0
  41. package/dist/graph/rules/terraform/cloud-run-services.js +61 -0
  42. package/dist/graph/rules/terraform/cloud-run-tasks-triggers.d.ts +14 -0
  43. package/dist/graph/rules/terraform/cloud-run-tasks-triggers.js +97 -0
  44. package/dist/graph/rules/terraform/event-topics.d.ts +18 -0
  45. package/dist/graph/rules/terraform/event-topics.js +193 -0
  46. package/dist/graph/rules/terraform/index.d.ts +5 -0
  47. package/dist/graph/rules/terraform/index.js +13 -0
  48. package/dist/graph/rules/terraform/spanner-databases.d.ts +17 -0
  49. package/dist/graph/rules/terraform/spanner-databases.js +180 -0
  50. package/dist/graph/spanner.d.ts +106 -0
  51. package/dist/graph/spanner.js +118 -0
  52. package/dist/graph/terraform-modules.d.ts +87 -0
  53. package/dist/graph/terraform-modules.js +107 -0
  54. package/package.json +27 -26
@@ -0,0 +1,93 @@
1
+ import { GraphOriginKind, } from '@causa/workspace-core';
2
+ import { tableId, tableLocator, databaseId, ModelFact, graphTemplate, } from '@causa/workspace-core/graph';
3
+ import { FIRESTORE_ENGINE, firestoreCollectionPath, firestoreDatabaseLocator, } from '../../ids.js';
4
+ /**
5
+ * The Firestore database of the workspace, `google.firestore.database`, and the collections schemas bind to:
6
+ *
7
+ * - A `database` node for the database.
8
+ * - A `table` node per Firestore collection a schema binds to (`causa.googleFirestoreCollection`). The locator is the
9
+ * collection path, with `{}` in place of document IDs (e.g. `users/{}/bookmarks`), under the single Firestore database
10
+ * of the workspace. A binding that does not name its root collection is reported.
11
+ * - A `realizes` edge from each collection to the entity whose binding declares it.
12
+ */
13
+ export const firestoreFromConfiguration = {
14
+ name: 'firestoreFromConfiguration',
15
+ kind: GraphOriginKind.Declared,
16
+ description: 'One `database` node from `google.firestore.database`, located by the database name. One `table` node per schema with a Firestore binding, located by its collection path under that database, and one `realizes` edge from the table to the entity.',
17
+ async run(graph) {
18
+ const { locator, context } = graph;
19
+ const model = await graph.get(ModelFact);
20
+ const nodes = [];
21
+ const edges = [];
22
+ const warnings = [];
23
+ const database = context
24
+ .asConfiguration()
25
+ .get('google.firestore.database');
26
+ if (typeof database === 'string') {
27
+ const data = {
28
+ engine: FIRESTORE_ENGINE,
29
+ resource: {
30
+ type: 'firestore.googleapis.com/Database',
31
+ id: graphTemplate('projects/', { configuration: 'google.project' }, '/databases/', { configuration: 'google.firestore.database' }),
32
+ },
33
+ };
34
+ nodes.push({
35
+ layer: 'infrastructure',
36
+ type: 'database',
37
+ locator: firestoreDatabaseLocator(database),
38
+ name: database,
39
+ sources: [
40
+ await locator.configurationSource(context, [
41
+ 'google',
42
+ 'firestore',
43
+ 'database',
44
+ ]),
45
+ ],
46
+ data,
47
+ });
48
+ }
49
+ for (const entity of model.entities.values()) {
50
+ const { schema } = entity;
51
+ const table = schema.definition.databases.find((d) => d.engine === FIRESTORE_ENGINE)?.table;
52
+ if (!table) {
53
+ continue;
54
+ }
55
+ const source = await locator.source(schema.file, [
56
+ ...schema.segments,
57
+ 'causa',
58
+ 'googleFirestoreCollection',
59
+ ]);
60
+ if (typeof database !== 'string') {
61
+ warnings.push({
62
+ message: `Schema '${schema.definition.name}' binds to Firestore but 'google.firestore.database' is not set.`,
63
+ sources: [source],
64
+ });
65
+ continue;
66
+ }
67
+ const collection = firestoreCollectionPath(table);
68
+ if (!collection) {
69
+ warnings.push({
70
+ message: `Schema '${schema.definition.name}' binds to Firestore documents '${table}', whose root collection is not named.`,
71
+ sources: [source],
72
+ });
73
+ continue;
74
+ }
75
+ const databaseLocator = firestoreDatabaseLocator(database);
76
+ nodes.push({
77
+ layer: 'infrastructure',
78
+ type: 'table',
79
+ locator: tableLocator(databaseLocator, collection),
80
+ parent: databaseId(databaseLocator),
81
+ name: collection,
82
+ sources: [source],
83
+ });
84
+ edges.push({
85
+ type: 'realizes',
86
+ from: tableId(databaseLocator, collection),
87
+ to: entity.id,
88
+ sources: [source],
89
+ });
90
+ }
91
+ return { nodes, edges, warnings };
92
+ },
93
+ };
@@ -0,0 +1,5 @@
1
+ import type { GraphRule } from '@causa/workspace-core';
2
+ /**
3
+ * The Google rules reading the workspace configuration and model, independently of the infrastructure code.
4
+ */
5
+ export declare const GOOGLE_GRAPH_RULES: readonly GraphRule[];
@@ -0,0 +1,11 @@
1
+ import { enqueuesAssumedFromGoogleTaskTrigger } from './enqueues-assumed-from-google-task-trigger.js';
2
+ import { firestoreFromConfiguration } from './firestore.js';
3
+ import { googleServiceContainerFromConfiguration } from './service-container.js';
4
+ /**
5
+ * The Google rules reading the workspace configuration and model, independently of the infrastructure code.
6
+ */
7
+ export const GOOGLE_GRAPH_RULES = [
8
+ firestoreFromConfiguration,
9
+ googleServiceContainerFromConfiguration,
10
+ enqueuesAssumedFromGoogleTaskTrigger,
11
+ ];
@@ -0,0 +1,13 @@
1
+ import { type GraphRule } from '@causa/workspace-core';
2
+ /**
3
+ * The elements declared by the Google-specific parts of the `serviceContainer` configuration of every service container
4
+ * project.
5
+ *
6
+ * - A `delivers` edge from a topic to each `google.pubSub` trigger consuming it. A trigger on a topic that no event
7
+ * schema defines has no topic node to come from: its edge only exists in the infrastructure layer.
8
+ * - An `accesses` edge from a project to each entity whose table lives in a datastore the project owns.
9
+ * `outputs['google.spanner']` lists databases (`<instance>.<database>`), and `outputs['google.firestore']` lists root
10
+ * collections. A Spanner entity belongs to the database whose DDL creates its table. A Firestore entity belongs to
11
+ * the root collection of its path.
12
+ */
13
+ export declare const googleServiceContainerFromConfiguration: GraphRule;
@@ -0,0 +1,123 @@
1
+ import { GraphOriginKind, } from '@causa/workspace-core';
2
+ import { DomainsFact, ModelFact, projectId, ProjectsFact, topicId, triggerId, } from '@causa/workspace-core/graph';
3
+ import { FIRESTORE_ENGINE, firestoreCollectionPath, SPANNER_ENGINE, } from '../../ids.js';
4
+ import { ddlSource, resolveSpannerTable, SpannerFact } from '../../spanner.js';
5
+ /**
6
+ * The elements declared by the Google-specific parts of the `serviceContainer` configuration of every service container
7
+ * project.
8
+ *
9
+ * - A `delivers` edge from a topic to each `google.pubSub` trigger consuming it. A trigger on a topic that no event
10
+ * schema defines has no topic node to come from: its edge only exists in the infrastructure layer.
11
+ * - An `accesses` edge from a project to each entity whose table lives in a datastore the project owns.
12
+ * `outputs['google.spanner']` lists databases (`<instance>.<database>`), and `outputs['google.firestore']` lists root
13
+ * collections. A Spanner entity belongs to the database whose DDL creates its table. A Firestore entity belongs to
14
+ * the root collection of its path.
15
+ */
16
+ export const googleServiceContainerFromConfiguration = {
17
+ name: 'googleServiceContainerFromConfiguration',
18
+ kind: GraphOriginKind.Declared,
19
+ description: 'One `delivers` edge from a topic to each trigger of type `google.pubSub` naming it, when an event schema defines the topic. One `accesses` edge from a project to each entity persisted in a Spanner database or a Firestore collection listed in its `serviceContainer.outputs`.',
20
+ async run(graph) {
21
+ const { locator } = graph;
22
+ const [domains, projects, model, spanner] = await Promise.all([
23
+ graph.get(DomainsFact),
24
+ graph.get(ProjectsFact),
25
+ graph.get(ModelFact),
26
+ graph.get(SpannerFact),
27
+ ]);
28
+ const edges = [];
29
+ const warnings = [];
30
+ const entities = [...model.entities.values()];
31
+ const spannerEntities = entities.flatMap((entity) => {
32
+ const resolution = resolveSpannerTable(spanner, entity, domains);
33
+ return resolution?.kind === 'resolved' ? [{ entity, resolution }] : [];
34
+ });
35
+ for (const project of projects.filter((p) => p.type === 'serviceContainer')) {
36
+ const configuration = project.context.asConfiguration();
37
+ const triggers = configuration.get('serviceContainer.triggers', { unsafe: true }) ?? {};
38
+ for (const [name, trigger] of Object.entries(triggers)) {
39
+ if (trigger.type !== 'google.pubSub' ||
40
+ typeof trigger.topic !== 'string' ||
41
+ !model.topics.has(trigger.topic)) {
42
+ continue;
43
+ }
44
+ edges.push({
45
+ type: 'delivers',
46
+ from: topicId(trigger.topic),
47
+ to: triggerId(project.directory, name),
48
+ sources: [
49
+ await locator.configurationSource(project.context, [
50
+ 'serviceContainer',
51
+ 'triggers',
52
+ name,
53
+ 'topic',
54
+ ]),
55
+ ],
56
+ });
57
+ }
58
+ const outputs = configuration.get('serviceContainer.outputs', {
59
+ unsafe: true,
60
+ });
61
+ const databases = (outputs?.[SPANNER_ENGINE] ?? []);
62
+ for (const [index, database] of databases.entries()) {
63
+ const source = await locator.configurationSource(project.context, [
64
+ 'serviceContainer',
65
+ 'outputs',
66
+ SPANNER_ENGINE,
67
+ index,
68
+ ]);
69
+ const owned = spannerEntities.filter(({ resolution }) => resolution.database.name === database);
70
+ if (owned.length === 0) {
71
+ warnings.push({
72
+ message: `Project '${project.name}' owns Spanner database '${database}' but no entity resolves to it.`,
73
+ sources: [source],
74
+ });
75
+ }
76
+ for (const { entity, resolution } of owned) {
77
+ edges.push({
78
+ type: 'accesses',
79
+ from: projectId(project.directory),
80
+ to: entity.id,
81
+ sources: [source, ddlSource(resolution.table)],
82
+ });
83
+ }
84
+ }
85
+ const collections = (outputs?.[FIRESTORE_ENGINE] ?? []);
86
+ for (const [index, collection] of collections.entries()) {
87
+ const source = await locator.configurationSource(project.context, [
88
+ 'serviceContainer',
89
+ 'outputs',
90
+ FIRESTORE_ENGINE,
91
+ index,
92
+ ]);
93
+ const owned = entities.filter((entity) => {
94
+ const table = entity.schema.definition.databases.find((d) => d.engine === FIRESTORE_ENGINE)?.table;
95
+ return (!!table &&
96
+ firestoreCollectionPath(table)?.split('/')[0] === collection);
97
+ });
98
+ if (owned.length === 0) {
99
+ warnings.push({
100
+ message: `Project '${project.name}' owns Firestore collection '${collection}' but no schema binds to it.`,
101
+ sources: [source],
102
+ });
103
+ }
104
+ for (const entity of owned) {
105
+ edges.push({
106
+ type: 'accesses',
107
+ from: projectId(project.directory),
108
+ to: entity.id,
109
+ sources: [
110
+ source,
111
+ await locator.source(entity.schema.file, [
112
+ ...entity.schema.segments,
113
+ 'causa',
114
+ 'googleFirestoreCollection',
115
+ ]),
116
+ ],
117
+ });
118
+ }
119
+ }
120
+ }
121
+ return { edges, warnings };
122
+ },
123
+ };
@@ -0,0 +1,15 @@
1
+ import { type GraphRule } from '@causa/workspace-core';
2
+ /**
3
+ * The source of the Causa Terraform module creating the load balancer routing requests to services.
4
+ */
5
+ export declare const API_ROUTER_MODULE_SOURCE = "causa-io/api-router/google";
6
+ /**
7
+ * The resources created by the blocks of the API router module:
8
+ *
9
+ * - An `apiRouter` node per block, located by its `name` argument (default `api`). The module does not read the
10
+ * workspace configuration: the GCP project of its URL map is assumed to be `google.project` when the
11
+ * `gcp_project_id` argument cannot be evaluated.
12
+ * - A `routes` edge from the router to each service whose Cloud Run module block sets `enable_public_http_endpoints`,
13
+ * with the project's `serviceContainer.endpoints.http` as paths.
14
+ */
15
+ export declare const apiRouterModule: GraphRule;
@@ -0,0 +1,117 @@
1
+ import { GraphOriginKind, } from '@causa/workspace-core';
2
+ import { apiRouterId, nodeId } from '@causa/workspace-core/graph';
3
+ import { modulesWithSource, terraformDeploys, TerraformProjectsFact, terraformValue, } from '@causa/workspace-terraform';
4
+ import { CloudRunFact } from '../../cloud-run.js';
5
+ import { blockResource, argumentOrConfiguration, } from '../../terraform-modules.js';
6
+ /**
7
+ * The source of the Causa Terraform module creating the load balancer routing requests to services.
8
+ */
9
+ export const API_ROUTER_MODULE_SOURCE = 'causa-io/api-router/google';
10
+ /**
11
+ * The default name of the API router.
12
+ */
13
+ const DEFAULT_API_ROUTER_NAME = 'api';
14
+ /**
15
+ * Returns the name of an API router, from its `name` argument or the module's default.
16
+ */
17
+ function routerName(block) {
18
+ const argument = block.arguments['name'];
19
+ if (argument === undefined) {
20
+ return DEFAULT_API_ROUTER_NAME;
21
+ }
22
+ const value = terraformValue(argument);
23
+ return typeof value === 'string' ? value : undefined;
24
+ }
25
+ /**
26
+ * The resources created by the blocks of the API router module:
27
+ *
28
+ * - An `apiRouter` node per block, located by its `name` argument (default `api`). The module does not read the
29
+ * workspace configuration: the GCP project of its URL map is assumed to be `google.project` when the
30
+ * `gcp_project_id` argument cannot be evaluated.
31
+ * - A `routes` edge from the router to each service whose Cloud Run module block sets `enable_public_http_endpoints`,
32
+ * with the project's `serviceContainer.endpoints.http` as paths.
33
+ */
34
+ export const apiRouterModule = {
35
+ name: 'apiRouterModule',
36
+ kind: GraphOriginKind.Declared,
37
+ description: 'One `apiRouter` node per API router module block, located by its `name` argument or the module default `api`, and linked by a `deploys` edge from the projects applying the block. One `routes` edge from the router to each service whose Cloud Run module block enables public HTTP endpoints, with the project’s `endpoints.http` as paths, assuming the single router is wired to them.',
38
+ async run(graph) {
39
+ const { locator } = graph;
40
+ const [terraformProjects, cloudRun] = await Promise.all([
41
+ graph.get(TerraformProjectsFact),
42
+ graph.get(CloudRunFact),
43
+ ]);
44
+ const apiRouters = modulesWithSource(terraformProjects, API_ROUTER_MODULE_SOURCE);
45
+ const nodes = [];
46
+ const edges = [];
47
+ const warnings = [];
48
+ for (const block of apiRouters) {
49
+ const source = block.declaration;
50
+ const name = routerName(block) ?? DEFAULT_API_ROUTER_NAME;
51
+ if (!routerName(block)) {
52
+ warnings.push({
53
+ message: `The router name is computed. Assuming '${DEFAULT_API_ROUTER_NAME}'.`,
54
+ sources: [source],
55
+ });
56
+ }
57
+ const part = argumentOrConfiguration(block, 'gcp_project_id', 'google.project', warnings);
58
+ nodes.push({
59
+ layer: 'infrastructure',
60
+ type: 'apiRouter',
61
+ locator: name,
62
+ name,
63
+ sources: [source],
64
+ data: {
65
+ resource: await blockResource(graph, block, {
66
+ type: 'compute.googleapis.com/UrlMap',
67
+ id: ['projects/', part, `/global/urlMaps/${name}`],
68
+ }, warnings),
69
+ },
70
+ });
71
+ edges.push(...terraformDeploys(block, nodeId('apiRouter', name)));
72
+ }
73
+ const exposed = cloudRun.filter((s) => s.enablePublicHttpEndpoints);
74
+ if (exposed.length === 0) {
75
+ return { nodes, edges, warnings };
76
+ }
77
+ if (apiRouters.length !== 1) {
78
+ warnings.push({
79
+ message: `There are ${apiRouters.length} API router module blocks. Cannot tell which one routes to ${exposed
80
+ .map((s) => `'${s.name}'`)
81
+ .join(', ')}.`,
82
+ sources: apiRouters.map((b) => b.declaration),
83
+ });
84
+ return { nodes, edges, warnings };
85
+ }
86
+ const [router] = apiRouters;
87
+ const name = routerName(router) ?? DEFAULT_API_ROUTER_NAME;
88
+ for (const service of exposed) {
89
+ const paths = service.project.context
90
+ .asConfiguration()
91
+ .get('serviceContainer.endpoints.http') ?? [];
92
+ if (paths.length === 0) {
93
+ warnings.push({
94
+ message: `Service '${service.name}' enables public HTTP endpoints but its project declares no 'endpoints.http'.`,
95
+ sources: [service.block.declaration],
96
+ });
97
+ }
98
+ const data = { paths };
99
+ edges.push({
100
+ type: 'routes',
101
+ from: apiRouterId(name),
102
+ to: service.id,
103
+ sources: [
104
+ service.block.declaration,
105
+ await locator.configurationSource(service.project.context, [
106
+ 'serviceContainer',
107
+ 'endpoints',
108
+ 'http',
109
+ ]),
110
+ router.declaration,
111
+ ],
112
+ data,
113
+ });
114
+ }
115
+ return { nodes, edges, warnings };
116
+ },
117
+ };
@@ -0,0 +1,11 @@
1
+ import { type GraphRuleOutput } from '@causa/workspace-core';
2
+ import { type GraphContext } from '@causa/workspace-core/graph';
3
+ /**
4
+ * A `scheduledJob` node per cron trigger of a project, for each Cloud Run module block deploying it with cron triggers
5
+ * enabled. The module names the job `run-<service>-<trigger>`. The locator is the trigger's. The job `realizes` its
6
+ * trigger, and `targets` the service the block deploys.
7
+ *
8
+ * @param graph The context of the extraction.
9
+ * @returns The scheduled jobs, and their edges.
10
+ */
11
+ export declare function cloudRunCronTriggers(graph: GraphContext): Promise<GraphRuleOutput>;
@@ -0,0 +1,67 @@
1
+ import {} from '@causa/workspace-core';
2
+ import { nodeId, scheduledJobId, triggerId, triggerLocator, } from '@causa/workspace-core/graph';
3
+ import { terraformDeploys } from '@causa/workspace-terraform';
4
+ import { CloudRunFact } from '../../cloud-run.js';
5
+ import { blockResource } from '../../terraform-modules.js';
6
+ /**
7
+ * A `scheduledJob` node per cron trigger of a project, for each Cloud Run module block deploying it with cron triggers
8
+ * enabled. The module names the job `run-<service>-<trigger>`. The locator is the trigger's. The job `realizes` its
9
+ * trigger, and `targets` the service the block deploys.
10
+ *
11
+ * @param graph The context of the extraction.
12
+ * @returns The scheduled jobs, and their edges.
13
+ */
14
+ export async function cloudRunCronTriggers(graph) {
15
+ const { locator } = graph;
16
+ const cloudRun = await graph.get(CloudRunFact);
17
+ const nodes = [];
18
+ const edges = [];
19
+ const warnings = [];
20
+ for (const service of cloudRun) {
21
+ const source = service.block.declaration;
22
+ for (const [name, trigger] of service.cronTriggers) {
23
+ const jobLocator = triggerLocator(service.project.directory, name);
24
+ const job = `run-${service.name}-${name}`;
25
+ nodes.push({
26
+ layer: 'infrastructure',
27
+ type: 'scheduledJob',
28
+ locator: jobLocator,
29
+ name: job,
30
+ sources: [
31
+ source,
32
+ await locator.configurationSource(service.project.context, [
33
+ 'serviceContainer',
34
+ 'triggers',
35
+ name,
36
+ ]),
37
+ ],
38
+ data: {
39
+ resource: await blockResource(graph, service.block, {
40
+ type: 'cloudscheduler.googleapis.com/Job',
41
+ scope: service.project,
42
+ id: [
43
+ 'projects/',
44
+ service.projectPart,
45
+ '/locations/',
46
+ service.locationPart,
47
+ `/jobs/${job}`,
48
+ ],
49
+ }, warnings),
50
+ },
51
+ });
52
+ edges.push(...terraformDeploys(service.block, nodeId('scheduledJob', jobLocator)), {
53
+ type: 'realizes',
54
+ from: scheduledJobId(jobLocator),
55
+ to: triggerId(service.project.directory, name),
56
+ sources: [source],
57
+ }, {
58
+ type: 'targets',
59
+ from: scheduledJobId(jobLocator),
60
+ to: service.id,
61
+ label: trigger.endpoint?.path,
62
+ sources: [source],
63
+ });
64
+ }
65
+ }
66
+ return { nodes, edges, warnings };
67
+ }
@@ -0,0 +1,16 @@
1
+ import { type GraphRuleOutput } from '@causa/workspace-core';
2
+ import { type GraphContext } from '@causa/workspace-core/graph';
3
+ /**
4
+ * The edges granted by the IAM bindings of the Cloud Run module blocks deploying a project.
5
+ *
6
+ * - A `publishes` edge from every service of the project to each broker topic of its `outputs.eventTopics`, when any
7
+ * block grants the publisher role.
8
+ * - An `accesses` edge from every service of the project to each datastore of its outputs, when any block grants
9
+ * access. Spanner access is granted on each database. Firestore access is granted on
10
+ * the whole database, but is declared for the collections listed in the outputs: edges target the collections under
11
+ * each listed root.
12
+ *
13
+ * @param graph The context of the extraction.
14
+ * @returns The edges granted by the blocks.
15
+ */
16
+ export declare function cloudRunGrants(graph: GraphContext): Promise<GraphRuleOutput>;
@@ -0,0 +1,118 @@
1
+ import {} from '@causa/workspace-core';
2
+ import { brokerTopicId, databaseId, databaseLocator, ModelFact, tableId, } from '@causa/workspace-core/graph';
3
+ import { CloudRunFact } from '../../cloud-run.js';
4
+ import { FIRESTORE_ENGINE, firestoreCollectionPath, firestoreDatabaseLocator, SPANNER_ENGINE, } from '../../ids.js';
5
+ /**
6
+ * The edges granted by the IAM bindings of the Cloud Run module blocks deploying a project.
7
+ *
8
+ * - A `publishes` edge from every service of the project to each broker topic of its `outputs.eventTopics`, when any
9
+ * block grants the publisher role.
10
+ * - An `accesses` edge from every service of the project to each datastore of its outputs, when any block grants
11
+ * access. Spanner access is granted on each database. Firestore access is granted on
12
+ * the whole database, but is declared for the collections listed in the outputs: edges target the collections under
13
+ * each listed root.
14
+ *
15
+ * @param graph The context of the extraction.
16
+ * @returns The edges granted by the blocks.
17
+ */
18
+ export async function cloudRunGrants(graph) {
19
+ const { locator, context } = graph;
20
+ const [cloudRun, model] = await Promise.all([
21
+ graph.get(CloudRunFact),
22
+ graph.get(ModelFact),
23
+ ]);
24
+ const edges = [];
25
+ const warnings = [];
26
+ const firestoreDatabase = context
27
+ .asConfiguration()
28
+ .get('google.firestore.database');
29
+ const firestorePaths = [...model.entities.values()].flatMap((e) => {
30
+ const table = e.schema.definition.databases.find((d) => d.engine === FIRESTORE_ENGINE)?.table;
31
+ const path = table ? firestoreCollectionPath(table) : undefined;
32
+ return path ? [path] : [];
33
+ });
34
+ // What the project declares is granted to all its services as soon as one of its blocks grants it.
35
+ for (const [project, services] of Map.groupBy(cloudRun, (s) => s.project)) {
36
+ const configuration = project.context.asConfiguration();
37
+ const publisherBlocks = services
38
+ .filter((s) => s.setPubsubPermissions)
39
+ .map((s) => s.block.declaration);
40
+ if (publisherBlocks.length > 0) {
41
+ const topics = configuration.get('serviceContainer.outputs.eventTopics') ?? [];
42
+ for (const [index, topic] of topics.entries()) {
43
+ const source = await locator.configurationSource(project.context, [
44
+ 'serviceContainer',
45
+ 'outputs',
46
+ 'eventTopics',
47
+ index,
48
+ ]);
49
+ for (const service of services) {
50
+ edges.push({
51
+ type: 'publishes',
52
+ from: service.id,
53
+ to: brokerTopicId(topic),
54
+ sources: [...publisherBlocks, source],
55
+ });
56
+ }
57
+ }
58
+ }
59
+ const outputs = configuration.get('serviceContainer.outputs', {
60
+ unsafe: true,
61
+ });
62
+ const spannerBlocks = services
63
+ .filter((s) => s.setSpannerPermissions)
64
+ .map((s) => s.block.declaration);
65
+ if (spannerBlocks.length > 0) {
66
+ const databases = (outputs?.[SPANNER_ENGINE] ?? []);
67
+ for (const [index, database] of databases.entries()) {
68
+ const source = await locator.configurationSource(project.context, [
69
+ 'serviceContainer',
70
+ 'outputs',
71
+ SPANNER_ENGINE,
72
+ index,
73
+ ]);
74
+ for (const service of services) {
75
+ edges.push({
76
+ type: 'accesses',
77
+ from: service.id,
78
+ to: databaseId(databaseLocator(SPANNER_ENGINE, database)),
79
+ sources: [...spannerBlocks, source],
80
+ });
81
+ }
82
+ }
83
+ }
84
+ const firestoreBlocks = services
85
+ .filter((s) => s.setFirestorePermissions)
86
+ .map((s) => s.block.declaration);
87
+ if (firestoreBlocks.length === 0 || !firestoreDatabase) {
88
+ continue;
89
+ }
90
+ const collections = (outputs?.[FIRESTORE_ENGINE] ?? []);
91
+ for (const [index, collection] of collections.entries()) {
92
+ const source = await locator.configurationSource(project.context, [
93
+ 'serviceContainer',
94
+ 'outputs',
95
+ FIRESTORE_ENGINE,
96
+ index,
97
+ ]);
98
+ const paths = new Set(firestorePaths.filter((p) => p.split('/')[0] === collection));
99
+ if (paths.size === 0) {
100
+ warnings.push({
101
+ message: `Project '${project.name}' is granted Firestore access for '${collection}' but no schema binds to that collection.`,
102
+ sources: [source],
103
+ });
104
+ }
105
+ for (const path of paths) {
106
+ for (const service of services) {
107
+ edges.push({
108
+ type: 'accesses',
109
+ from: service.id,
110
+ to: tableId(firestoreDatabaseLocator(firestoreDatabase), path),
111
+ sources: [...firestoreBlocks, source],
112
+ });
113
+ }
114
+ }
115
+ }
116
+ }
117
+ return { edges, warnings };
118
+ }
@@ -0,0 +1,6 @@
1
+ import { type GraphRule } from '@causa/workspace-core';
2
+ /**
3
+ * The resources created by the blocks of the Cloud Run module, each part following a file of the module: the service,
4
+ * the Pub/Sub, tasks, and cron triggers, and the IAM grants.
5
+ */
6
+ export declare const cloudRunModule: GraphRule;
@@ -0,0 +1,29 @@
1
+ import { GraphOriginKind, } from '@causa/workspace-core';
2
+ import { cloudRunCronTriggers } from './cloud-run-cron-triggers.js';
3
+ import { cloudRunGrants } from './cloud-run-grants.js';
4
+ import { cloudRunPubSubTriggers } from './cloud-run-pubsub-triggers.js';
5
+ import { cloudRunServices } from './cloud-run-services.js';
6
+ import { cloudRunTasksTriggers } from './cloud-run-tasks-triggers.js';
7
+ /**
8
+ * The resources created by the blocks of the Cloud Run module, each part following a file of the module: the service,
9
+ * the Pub/Sub, tasks, and cron triggers, and the IAM grants.
10
+ */
11
+ export const cloudRunModule = {
12
+ name: 'cloudRunModule',
13
+ kind: GraphOriginKind.Declared,
14
+ description: 'The resources each Cloud Run module block creates: the `service` realizing the project read back from `configuration_file`, a push `brokerSubscription` per Pub/Sub trigger, a `queue` per task trigger, and a `scheduledJob` per cron trigger, with the edges between them, and the `publishes`, `accesses`, and `enqueues` edges its IAM bindings grant. Each resource is linked by a `deploys` edge from the projects applying the block.',
15
+ async run(graph) {
16
+ const outputs = await Promise.all([
17
+ cloudRunServices(graph),
18
+ cloudRunPubSubTriggers(graph),
19
+ cloudRunTasksTriggers(graph),
20
+ cloudRunCronTriggers(graph),
21
+ cloudRunGrants(graph),
22
+ ]);
23
+ return {
24
+ nodes: outputs.flatMap((o) => o.nodes ?? []),
25
+ edges: outputs.flatMap((o) => o.edges ?? []),
26
+ warnings: outputs.flatMap((o) => o.warnings ?? []),
27
+ };
28
+ },
29
+ };
@@ -0,0 +1,14 @@
1
+ import { type GraphRuleOutput } from '@causa/workspace-core';
2
+ import { type GraphContext } from '@causa/workspace-core/graph';
3
+ /**
4
+ * A push `brokerSubscription` per Pub/Sub trigger of a project, for each Cloud Run module block deploying it with
5
+ * Pub/Sub triggers enabled. The locator is the trigger's. The module names the subscription `run-<service>-<trigger>`.
6
+ *
7
+ * Each subscription `realizes` its trigger, `targets` the service its module block deploys, and is delivered to by the
8
+ * broker topic of its trigger. A topic that no event schema of the workspace defines is created outside of it, and
9
+ * gets its own `brokerTopic` node.
10
+ *
11
+ * @param graph The context of the extraction.
12
+ * @returns The subscriptions, the external broker topics, and their edges.
13
+ */
14
+ export declare function cloudRunPubSubTriggers(graph: GraphContext): Promise<GraphRuleOutput>;