@causa/workspace-google 1.3.1 → 1.4.0-beta.2

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 (98) hide show
  1. package/README.md +23 -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-app-check/generate-token.js +3 -1
  5. package/dist/functions/google-firebase-storage/merge-rules.js +3 -1
  6. package/dist/functions/google-firestore/generate-typescript-decorators.js +2 -1
  7. package/dist/functions/google-firestore/merge-rules.js +3 -1
  8. package/dist/functions/google-identity-platform/generate-custom-token.js +7 -2
  9. package/dist/functions/google-pubsub/index.d.ts +1 -1
  10. package/dist/functions/google-pubsub/index.js +1 -1
  11. package/dist/functions/google-pubsub/write-topics.d.ts +7 -0
  12. package/dist/functions/google-pubsub/write-topics.js +17 -3
  13. package/dist/functions/google-services/enable.js +3 -1
  14. package/dist/functions/google-spanner/generate-typescript-decorators.js +2 -1
  15. package/dist/functions/google-spanner/list-databases.d.ts +1 -1
  16. package/dist/functions/google-spanner/write-databases.js +7 -2
  17. package/dist/functions/graph/fetch-environment-metric-series.call.d.ts +3 -0
  18. package/dist/functions/graph/fetch-environment-metric-series.call.js +107 -0
  19. package/dist/functions/graph/fetch-environment-metric-series.d.ts +23 -0
  20. package/dist/functions/graph/fetch-environment-metric-series.js +40 -0
  21. package/dist/functions/graph/get-environment-provider.d.ts +11 -0
  22. package/dist/functions/graph/get-environment-provider.js +20 -0
  23. package/dist/functions/graph/index.d.ts +3 -0
  24. package/dist/functions/graph/index.js +3 -0
  25. package/dist/functions/graph/list-rules.d.ts +11 -0
  26. package/dist/functions/graph/list-rules.js +16 -0
  27. package/dist/functions/index.js +2 -1
  28. package/dist/graph/cloud-run.d.ts +98 -0
  29. package/dist/graph/cloud-run.js +102 -0
  30. package/dist/graph/environment/fetchers/cloud-monitoring-alerts.d.ts +6 -0
  31. package/dist/graph/environment/fetchers/cloud-monitoring-alerts.js +106 -0
  32. package/dist/graph/environment/fetchers/cloud-monitoring.d.ts +5 -0
  33. package/dist/graph/environment/fetchers/cloud-monitoring.js +13 -0
  34. package/dist/graph/environment/fetchers/cloud-run.d.ts +5 -0
  35. package/dist/graph/environment/fetchers/cloud-run.js +88 -0
  36. package/dist/graph/environment/fetchers/cloud-scheduler.d.ts +5 -0
  37. package/dist/graph/environment/fetchers/cloud-scheduler.js +95 -0
  38. package/dist/graph/environment/fetchers/console-links.d.ts +14 -0
  39. package/dist/graph/environment/fetchers/console-links.js +132 -0
  40. package/dist/graph/environment/fetchers/error-reporting.d.ts +5 -0
  41. package/dist/graph/environment/fetchers/error-reporting.js +98 -0
  42. package/dist/graph/environment/fetchers/index.d.ts +5 -0
  43. package/dist/graph/environment/fetchers/index.js +17 -0
  44. package/dist/graph/environment/index.d.ts +9 -0
  45. package/dist/graph/environment/index.js +9 -0
  46. package/dist/graph/environment/load-balancer-backends.d.ts +82 -0
  47. package/dist/graph/environment/load-balancer-backends.js +126 -0
  48. package/dist/graph/environment/monitoring-alerts.d.ts +60 -0
  49. package/dist/graph/environment/monitoring-alerts.js +67 -0
  50. package/dist/graph/environment/monitoring-catalog.d.ts +95 -0
  51. package/dist/graph/environment/monitoring-catalog.js +356 -0
  52. package/dist/graph/environment/monitoring-mappings.d.ts +133 -0
  53. package/dist/graph/environment/monitoring-mappings.js +97 -0
  54. package/dist/graph/environment/monitoring-query.d.ts +116 -0
  55. package/dist/graph/environment/monitoring-query.js +314 -0
  56. package/dist/graph/environment/monitoring-values.d.ts +41 -0
  57. package/dist/graph/environment/monitoring-values.js +119 -0
  58. package/dist/graph/environment/resource-resolvers.d.ts +7 -0
  59. package/dist/graph/environment/resource-resolvers.js +57 -0
  60. package/dist/graph/environment/resources.d.ts +45 -0
  61. package/dist/graph/environment/resources.js +49 -0
  62. package/dist/graph/ids.d.ts +35 -0
  63. package/dist/graph/ids.js +44 -0
  64. package/dist/graph/index.d.ts +7 -0
  65. package/dist/graph/index.js +7 -0
  66. package/dist/graph/rules/configuration/enqueues-assumed-from-google-task-trigger.d.ts +6 -0
  67. package/dist/graph/rules/configuration/enqueues-assumed-from-google-task-trigger.js +39 -0
  68. package/dist/graph/rules/configuration/firestore.d.ts +11 -0
  69. package/dist/graph/rules/configuration/firestore.js +93 -0
  70. package/dist/graph/rules/configuration/index.d.ts +5 -0
  71. package/dist/graph/rules/configuration/index.js +11 -0
  72. package/dist/graph/rules/configuration/service-container.d.ts +13 -0
  73. package/dist/graph/rules/configuration/service-container.js +123 -0
  74. package/dist/graph/rules/terraform/api-router.d.ts +15 -0
  75. package/dist/graph/rules/terraform/api-router.js +117 -0
  76. package/dist/graph/rules/terraform/cloud-run-cron-triggers.d.ts +11 -0
  77. package/dist/graph/rules/terraform/cloud-run-cron-triggers.js +67 -0
  78. package/dist/graph/rules/terraform/cloud-run-grants.d.ts +16 -0
  79. package/dist/graph/rules/terraform/cloud-run-grants.js +118 -0
  80. package/dist/graph/rules/terraform/cloud-run-module.d.ts +6 -0
  81. package/dist/graph/rules/terraform/cloud-run-module.js +29 -0
  82. package/dist/graph/rules/terraform/cloud-run-pubsub-triggers.d.ts +14 -0
  83. package/dist/graph/rules/terraform/cloud-run-pubsub-triggers.js +103 -0
  84. package/dist/graph/rules/terraform/cloud-run-services.d.ts +12 -0
  85. package/dist/graph/rules/terraform/cloud-run-services.js +61 -0
  86. package/dist/graph/rules/terraform/cloud-run-tasks-triggers.d.ts +14 -0
  87. package/dist/graph/rules/terraform/cloud-run-tasks-triggers.js +97 -0
  88. package/dist/graph/rules/terraform/event-topics.d.ts +18 -0
  89. package/dist/graph/rules/terraform/event-topics.js +193 -0
  90. package/dist/graph/rules/terraform/index.d.ts +5 -0
  91. package/dist/graph/rules/terraform/index.js +13 -0
  92. package/dist/graph/rules/terraform/spanner-databases.d.ts +17 -0
  93. package/dist/graph/rules/terraform/spanner-databases.js +180 -0
  94. package/dist/graph/spanner.d.ts +106 -0
  95. package/dist/graph/spanner.js +118 -0
  96. package/dist/graph/terraform-modules.d.ts +87 -0
  97. package/dist/graph/terraform-modules.js +107 -0
  98. package/package.json +30 -29
@@ -0,0 +1,45 @@
1
+ import type { GraphEnvironmentContext, GraphNodeEntry } from '@causa/workspace-core/graph';
2
+ /**
3
+ * The segments of a Google Cloud resource name, keyed by collection.
4
+ * For example, `projects/my-project/locations/europe-west1/services/my-service` is
5
+ * `{ projects: 'my-project', locations: 'europe-west1', services: 'my-service' }`.
6
+ */
7
+ export type ResourceNameSegments = Record<string, string>;
8
+ /**
9
+ * Parses a Google Cloud resource name into its segments.
10
+ * The `global` segment, which is not followed by an ID (e.g. in `projects/my-project/global/urlMaps/api`), is skipped.
11
+ *
12
+ * @param name The resource name.
13
+ * @returns The segments, keyed by collection.
14
+ */
15
+ export declare function parseResourceName(name: string): ResourceNameSegments;
16
+ /**
17
+ * A node whose resource is a Google Cloud resource, along with its full name.
18
+ */
19
+ export type GoogleResource = {
20
+ /**
21
+ * The node.
22
+ */
23
+ readonly node: GraphNodeEntry;
24
+ /**
25
+ * The type of the resource, e.g. `run.googleapis.com/Service`.
26
+ */
27
+ readonly type: string;
28
+ /**
29
+ * The full resource name.
30
+ */
31
+ readonly name: string;
32
+ /**
33
+ * The segments of the resource name.
34
+ */
35
+ readonly segments: ResourceNameSegments;
36
+ };
37
+ /**
38
+ * Returns the Google Cloud resources of the graph, with the segments of their names.
39
+ *
40
+ * @param environment The context of the environment.
41
+ * @param resourceType The type of the resources. Defaults to all Google Cloud resource types.
42
+ * @param nodeType The type of the nodes. Defaults to all node types.
43
+ * @returns The resources, in the order of the graph.
44
+ */
45
+ export declare function googleResources(environment: GraphEnvironmentContext, resourceType?: string, nodeType?: string): GoogleResource[];
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Parses a Google Cloud resource name into its segments.
3
+ * The `global` segment, which is not followed by an ID (e.g. in `projects/my-project/global/urlMaps/api`), is skipped.
4
+ *
5
+ * @param name The resource name.
6
+ * @returns The segments, keyed by collection.
7
+ */
8
+ export function parseResourceName(name) {
9
+ const tokens = name.split('/');
10
+ const segments = {};
11
+ for (let index = 0; index < tokens.length;) {
12
+ const collection = tokens[index];
13
+ if (collection === 'global') {
14
+ index += 1;
15
+ continue;
16
+ }
17
+ segments[collection] = tokens[index + 1] ?? '';
18
+ index += 2;
19
+ }
20
+ return segments;
21
+ }
22
+ /**
23
+ * Returns whether a resource type is the type of a Google Cloud resource.
24
+ *
25
+ * @param type The type of the resource.
26
+ * @returns `true` for Google Cloud resource types, e.g. `run.googleapis.com/Service`.
27
+ */
28
+ function isGoogleResourceType(type) {
29
+ return type.includes('.googleapis.com/');
30
+ }
31
+ /**
32
+ * Returns the Google Cloud resources of the graph, with the segments of their names.
33
+ *
34
+ * @param environment The context of the environment.
35
+ * @param resourceType The type of the resources. Defaults to all Google Cloud resource types.
36
+ * @param nodeType The type of the nodes. Defaults to all node types.
37
+ * @returns The resources, in the order of the graph.
38
+ */
39
+ export function googleResources(environment, resourceType, nodeType) {
40
+ return environment
41
+ .resources(resourceType, nodeType)
42
+ .filter(({ resource }) => isGoogleResourceType(resource.type))
43
+ .map(({ node, resource: { type, id } }) => ({
44
+ node,
45
+ type,
46
+ name: id,
47
+ segments: parseResourceName(id),
48
+ }));
49
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The engine of Spanner databases. It is also the key of `serviceContainer.outputs` listing the databases a project
3
+ * owns, and the `engine` of Spanner database bindings.
4
+ */
5
+ export declare const SPANNER_ENGINE = "google.spanner";
6
+ /**
7
+ * The engine of Firestore databases. It is also the key of `serviceContainer.outputs` listing the collections a
8
+ * project owns, and the `engine` of Firestore database bindings.
9
+ */
10
+ export declare const FIRESTORE_ENGINE = "google.firestore";
11
+ /**
12
+ * The engine of BigQuery datasets.
13
+ */
14
+ export declare const BIGQUERY_ENGINE = "google.bigquery";
15
+ export declare const spannerInstanceId: (instance: string) => string;
16
+ /**
17
+ * The name of a Spanner database within its engine, `<instance>.<database>`, as `outputs['google.spanner']` lists it.
18
+ */
19
+ export declare const spannerDatabaseName: (instance: string, database: string) => string;
20
+ export declare const spannerDatabaseLocator: (instance: string, database: string) => string;
21
+ export declare const firestoreDatabaseLocator: (database: string) => string;
22
+ export declare const bigQueryDatasetLocator: (dataset: string) => string;
23
+ /**
24
+ * Returns the path of the collection holding the documents of a Firestore binding, independently of the properties
25
+ * used as document IDs: the last segment (the document) is dropped, and the other placeholders are replaced with `{}`.
26
+ * For example, `users/{user}/bookmarkFolders/{id}` becomes `users/{}/bookmarkFolders`.
27
+ *
28
+ * @param bindingTable The table of the binding, i.e. the path of its documents.
29
+ * @returns The collection path, or `undefined` if the root collection is not named by the binding.
30
+ */
31
+ export declare function firestoreCollectionPath(bindingTable: string): string | undefined;
32
+ /**
33
+ * The locator of the BigQuery subscription archiving a topic's raw events.
34
+ */
35
+ export declare const rawEventsSubscriptionLocator: (topic: string) => string;
@@ -0,0 +1,44 @@
1
+ import { databaseLocator, nodeId } from '@causa/workspace-core/graph';
2
+ /**
3
+ * The engine of Spanner databases. It is also the key of `serviceContainer.outputs` listing the databases a project
4
+ * owns, and the `engine` of Spanner database bindings.
5
+ */
6
+ export const SPANNER_ENGINE = 'google.spanner';
7
+ /**
8
+ * The engine of Firestore databases. It is also the key of `serviceContainer.outputs` listing the collections a
9
+ * project owns, and the `engine` of Firestore database bindings.
10
+ */
11
+ export const FIRESTORE_ENGINE = 'google.firestore';
12
+ /**
13
+ * The engine of BigQuery datasets.
14
+ */
15
+ export const BIGQUERY_ENGINE = 'google.bigquery';
16
+ export const spannerInstanceId = (instance) => nodeId('google.spanner.instance', instance);
17
+ /**
18
+ * The name of a Spanner database within its engine, `<instance>.<database>`, as `outputs['google.spanner']` lists it.
19
+ */
20
+ export const spannerDatabaseName = (instance, database) => `${instance}.${database}`;
21
+ export const spannerDatabaseLocator = (instance, database) => databaseLocator(SPANNER_ENGINE, spannerDatabaseName(instance, database));
22
+ export const firestoreDatabaseLocator = (database) => databaseLocator(FIRESTORE_ENGINE, database);
23
+ export const bigQueryDatasetLocator = (dataset) => databaseLocator(BIGQUERY_ENGINE, dataset);
24
+ /**
25
+ * Returns the path of the collection holding the documents of a Firestore binding, independently of the properties
26
+ * used as document IDs: the last segment (the document) is dropped, and the other placeholders are replaced with `{}`.
27
+ * For example, `users/{user}/bookmarkFolders/{id}` becomes `users/{}/bookmarkFolders`.
28
+ *
29
+ * @param bindingTable The table of the binding, i.e. the path of its documents.
30
+ * @returns The collection path, or `undefined` if the root collection is not named by the binding.
31
+ */
32
+ export function firestoreCollectionPath(bindingTable) {
33
+ const segments = bindingTable.split('/').slice(0, -1);
34
+ if (segments.length === 0 || /^\{.*\}$/.test(segments[0])) {
35
+ return undefined;
36
+ }
37
+ return segments
38
+ .map((segment) => (/^\{.*\}$/.test(segment) ? '{}' : segment))
39
+ .join('/');
40
+ }
41
+ /**
42
+ * The locator of the BigQuery subscription archiving a topic's raw events.
43
+ */
44
+ export const rawEventsSubscriptionLocator = (topic) => `rawEvents/${topic}`;
@@ -0,0 +1,7 @@
1
+ export * from './cloud-run.js';
2
+ export * from './environment/index.js';
3
+ export * from './ids.js';
4
+ export * from './rules/configuration/index.js';
5
+ export * from './rules/terraform/index.js';
6
+ export * from './spanner.js';
7
+ export * from './terraform-modules.js';
@@ -0,0 +1,7 @@
1
+ export * from './cloud-run.js';
2
+ export * from './environment/index.js';
3
+ export * from './ids.js';
4
+ export * from './rules/configuration/index.js';
5
+ export * from './rules/terraform/index.js';
6
+ export * from './spanner.js';
7
+ export * from './terraform-modules.js';
@@ -0,0 +1,6 @@
1
+ import { type GraphRule } from '@causa/workspace-core';
2
+ /**
3
+ * An `enqueues` edge from a service container project to each of its own `google.task` or `google.tasks` triggers,
4
+ * assuming the owning project is the producer.
5
+ */
6
+ export declare const enqueuesAssumedFromGoogleTaskTrigger: GraphRule;
@@ -0,0 +1,39 @@
1
+ import { GraphOriginKind, } from '@causa/workspace-core';
2
+ import { projectId, triggerId, ProjectsFact, } from '@causa/workspace-core/graph';
3
+ /**
4
+ * An `enqueues` edge from a service container project to each of its own `google.task` or `google.tasks` triggers,
5
+ * assuming the owning project is the producer.
6
+ */
7
+ export const enqueuesAssumedFromGoogleTaskTrigger = {
8
+ name: 'enqueuesAssumedFromGoogleTaskTrigger',
9
+ kind: GraphOriginKind.Inferred,
10
+ description: 'One `enqueues` edge from a service container project to each of its own `google.task` or `google.tasks` triggers, assuming the owning project is the producer.',
11
+ async run(graph) {
12
+ const { locator } = graph;
13
+ const projects = await graph.get(ProjectsFact);
14
+ const edges = [];
15
+ for (const project of projects.filter((p) => p.type === 'serviceContainer')) {
16
+ const triggers = project.context
17
+ .asConfiguration()
18
+ .get('serviceContainer.triggers', { unsafe: true }) ?? {};
19
+ for (const [name, trigger] of Object.entries(triggers)) {
20
+ if (!['google.task', 'google.tasks'].includes(trigger.type ?? '')) {
21
+ continue;
22
+ }
23
+ edges.push({
24
+ type: 'enqueues',
25
+ from: projectId(project.directory),
26
+ to: triggerId(project.directory, name),
27
+ sources: [
28
+ await locator.configurationSource(project.context, [
29
+ 'serviceContainer',
30
+ 'triggers',
31
+ name,
32
+ ]),
33
+ ],
34
+ });
35
+ }
36
+ }
37
+ return { edges };
38
+ },
39
+ };
@@ -0,0 +1,11 @@
1
+ import { type GraphRule } from '@causa/workspace-core';
2
+ /**
3
+ * The Firestore database of the workspace, `google.firestore.database`, and the collections schemas bind to:
4
+ *
5
+ * - A `database` node for the database.
6
+ * - A `table` node per Firestore collection a schema binds to (`causa.googleFirestoreCollection`). The locator is the
7
+ * collection path, with `{}` in place of document IDs (e.g. `users/{}/bookmarks`), under the single Firestore database
8
+ * of the workspace. A binding that does not name its root collection is reported.
9
+ * - A `realizes` edge from each collection to the entity whose binding declares it.
10
+ */
11
+ export declare const firestoreFromConfiguration: GraphRule;
@@ -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>;